[토비의 스프링] 1장 오브젝트와 의존관계

책에서 소개하는 예제를 통해서 스프링은 왜이렇게 생겼는가 이해해 볼수 있는 챕터

스프링의 철학

  • ejb 가 커지면서 잃어버렸던 객체지향을 다시 잘 적용해보자
    • ejb 가 많은 기능을 제공하지만 서비스로직에 컨테이너를 쓰기위한 코드들이 많이 침투됐다함
  • 객체지향을 해야하니까 오브젝트에 많은 관심을 둔다.
    • 오브젝트가 어떻게 생기고 없어지고 사용되는지
    • 오브젝트를 다루는 베스트프랙티스를 프레임워크단에서 제공해버리기

첫번째, 관심사의 분리

왜 해야 하는가?

  • 소프트웨어는 여러가지 요구사항에 의해 항상 변화하고 개발자는 변화를 어느정도 대비하면서 개발을 해야함
  • 관심사를 분리하지 않으면 db 암호를 바꾸려고 모든 dao를 변경해야하는등 수정에 유연하지 못하게 됨
    응집도 높고 결합도 낮은 코드를 만들기 위해 노력하자

User 를 읽고 쓰는 UserDao 가 진짜 유저만 읽고 쓰게 만들어보는 과정

0단계 일단 되게..

public class UserDao {

    void void add(User user){
        // 커넥션 연결 책임
        Class.forName("driver")
        Connection c = DriverMnager.getConnection("jdbcUrl")

        // query 만드는 책임
        PreparedStatement ps = c.preapredStatment("sql query")
        ps.setString(1, user.getId())
        ps.setString(1, user.getId())
        ps.setString(1, user.getId())

        // query 실행하고 닫는 책임
        ps.excuteUpdate();
        ps.clode();

        // 커넥션 닫는 책임
        c.close();
    }
}

일단 되지만 함수에 책임이 너무 많고 여러가지 함수가 생기면 중복되는 코드들이 많다.

1단계 함수로 코드중복 제거

public void add(User user) {
        Connection c = getConnection()

        // query 만드는 책임
        PreparedStatement ps = c.preapredStatment("sql query")
        ps.setString(1, user.getId())
        ps.setString(1, user.getId())
        ps.setString(1, user.getId())

        // query 실행하고 닫는 책임
        ps.excuteUpdate();
        ps.clode();

        // 커넥션 닫는 책임
        c.close();
}

private Connection getConnection() {
    // 커넥션 연결 책임
        Class.forName("driver")
        Connection c = DriverMnager.getConnection("jdbcUrl")
    return connection
}

함수로 분리해서 커넥션 생성에 대한 책임을 덜어냈다. 개념적으론 커넥션에 대한 변경사항이 들어와도 다른 함수를 건들지 않는다.

2단계 상속으로 확장가능하게

public abstract class UserDao {
    public void add(User user) {}
    public void get(String id) {}

    public abstract Connection getConnection()
}

class UserDaoMySql extends UserDao {
        public abstract Connection getConnection(){
            // 커넥션생성
        }
}
class UserDaoOracle extends UserDao {
        public abstract Connection getConnection(){
            // 커넥션생성
        }
}

여기는 책임을 더 덜어내지는 않았지만 템플릿 메서드 패턴을 사용해서 함수분리만으로는 불가능한 커넥션 생성방법에 대한 확장을 이뤄냈다.

그리고 팩토리 메서드 패턴 사용해서 커넥션 생성에 대한 책임이 함수에서 하위 클래스로 옮겨졌다. 관심사가 함수보다 더 멀찍이 분리 됐다

3단계 합성을 사용해서 더 명확하게 분리

// 책임이 하나니까 네이밍도 아주 명확하다
public interface ConnectionMaker {
    public Connection getConnection()
}

class MySqlConnectionMaker implements ConnectionMaker {
    public Connection getConnection(){
            // 커넥션생성
        }
}

class OracleConnectionMaker implements ConnectionMaker {
    public Connection getConnection(){
            // 커넥션생성
        }
}

public class UserDao {
    private ConnectionMaker connectionMaker;

    public UserDao() {
        // 어떤 커넥션 만들지 고르는 책임은 아직 남아 있음
        connectionMaker = new MySqlConnectionMaker();
    }

    public void add(User user){
        Connection c = connectionMaker.getConnection();
        ...
    }
}

자식클래스에게 떠넘겼던 커넥션 생성의 책임을 상속의 단점을 버리고 더 명시적이고 화끈하게 다른 클래스로 분리한다. 그러기 위해서 상속을 버리고 합성을 선택한다. 그리고 확장도 놓칠 수 없으니 인터페이스도 적용한다.

4단계 클라이언트에 책임 떠넘기기

public class UserDao {
    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}

public class UserDaoTest { // 여기로 떠넘기기
    public void test(){ // 테스트
        ConnectionMaker connectionMaker = new MySqlConnectionMaker();

        UserDao dao = new UserDao(connectionMaker); 

        dao.add(new User())
    }
}

UserDao 를 사용하는 클라이언트 코드로 책임을 떠넘겨서 분리했다. 하지만 client는 함수를 테스트한다는 또 본인만의 책임이 있을것이기 때문에 바람직하지는 않다.

5단계 팩토리 적용해서 바람직하게 떠넘기기

어떤 커넥션을 만들지 결정하는 책임만 수행하는 클래스를 만들어서 책임을 분리한다. 그런 역할을 하는 애들을 팩토리 라고 한다.

// 여기로 떠넘기기
public class DaoFactory {
    public UserDao userDao(){
        ConnectionMaker connectionMaker = new MySqlConnectionMaker();
        UserDao dao = new UserDao(connectionMaker); 
        return dao;
    }
}

public class UserDaoTest { 
    public void test(){  
        // 커넥션 뭔지 모름
        UserDao dao = new DaoFactory().userDao();
        dao.add(new User())
    }
}

Ioc 제어의 역전
5단계를 거쳐 리팩터링하면서 계속 숨어있던 제어의 역전이라는 개념이 있다. 흐름 제어 구조가 뒤 바뀌는 것인데 UserDao가 모든 오브젝트를 생성하고 사용하며 모든 흐름을 제어하다가. 그 제어 흐름을 팩토리에 맡기고 본인의 역할만 수행함으로써 흐름제어가 뒤 바뀐 것이다.

그래서 Spring 은 왜쓰냐?

스프링에서 생성/조합 역할을 담당하는게 빈팩토리이고 그것을 확장한것이 ApplicationContext 이다.

그리고 관리되는 객체들을 스프링빈이라고 한다. (제어의 역전이 적용된 오브젝트들)

@Confinguration
public class DaoFactory {
    @Bean
    public UserDao userDao(){
        return new UserDao(connectionMaker()); 
    }

    @Bean
    public ConnectionMaker connectionMaker(){
        return new MySqlConnectionMaker();
    }
}

public class UserDaoTest { 
    public void test(){  
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao dao = context.getBean("userDao", UserDao.class);
    }
}

여기 까지만 보면 딱히 쓸 필요가 없고 더 복잡하기만 해보이지만 스프링컨테이너에 등록해서 쓰면 아래와 같은 장점이 있다.

1. 간편하게 싱글톤으로 관리해줘서 성능 이점이 있다.

수많은 요청마다 객체를 만들면 낭비이기 때문에 싱글톤 클래스로 만들어 쓰는걸 고려할 수 있다. 그래서 직접 싱글턴 클래스로 만들면 아래와 같은 한계를 만난다.

  • private 생성자라 상속 불가능
    • 객체지향의 이점을 누리지 못한다.
  • 테스트하기 힘들다
    • 모킹등이 쉽지 않다
  • 전역상태를 만들 수 있다.
    • 아무곳에서나 사용이가능해서 전역상태로 될수 있음
      • 스프링을 써도 똑같은게 아닌가 생각한다.
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다. 
    • 분산환경에서는 독립적으로 생겨서 싱글톤으로 가치가 떨어진다.
      • 스프링을 써도 똑같은게 아닌가 생각한다.
    • 클래스 로더 구성에 따라 싱글톤이 아니게 될 수 있음
// GPT 가 짜준 예제
public class Main {
    public static void main(String[] args) throws Exception {
        // 첫 번째 클래스 로더
        ClassLoader classLoader1 = new CustomClassLoader("loader1");
        Class<?> mySingletonClass1 = classLoader1.loadClass("MySingleton");
        Object instance1 = mySingletonClass1.getMethod("getInstance").invoke(null);

        // 두 번째 클래스 로더
        ClassLoader classLoader2 = new CustomClassLoader("loader2");
        Class<?> mySingletonClass2 = classLoader2.loadClass("MySingleton");
        Object instance2 = mySingletonClass2.getMethod("getInstance").invoke(null);

        System.out.println(instance1 == instance2); // false
    }
}

2. 클라이언트에서 구체적인 팩토리클래스를 몰라도 된다.

책에 있는 예제로는 잘 안 와닿아서

// 이거 bean이 아니라서 repository 주입을 못 받음
public class UserEntityListener {
    @PrePersist
    @PreUpdate
    public void prePersistAndPreUpdate(Object o){
        UserHistoryRepository userHistoryRepository = BeanUtils.getBean(UserHistoryRepository.class);

        User user = (User) o;
        UserHistory userHistory = new UserHistory();

        userHistoryRepository.save(userHistory);
    }
}

@Component
public class BeanUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        BeanUtils.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz){
        return applicationContext.getBean(clazz);
    }
}

여기 EntityListener에서 Repository 만드는 Factory를 몰라도 된다. 일관성있게 getBean 으로 bean을 얻을 수 있음(검색이 가능함)

3. DI의 장점을 쉽게 누릴수있게

우리가 자주 쓰는 @Autowired 자동주입 같은것을 프레임워크 단에서 지원하면서 쉽게 사용할수 있게 해준다. 추후에 나올 트랜잭션이나 aop 같은것도 DI의 장점을 많이 이용하는데 어노테이션만 가지고 쉽게 쓸수 있게 해주는 셈이다.

4. XML

xml 파일에 bean 구성정보를 담아서 코드 변경없이 의존성 변경 가능