본문 바로가기
Spring/Toby's Spring Reading Club

1-1장. 오브젝트와 의존관계

by Soono991 2022. 9. 5.

저는 개인적으로 1장에서 가장 흥미로웠던 부분은 1.1 ~ 1.4까지 이어지는 초난감 DAO의 리팩터링 과정이라고 생각합니다.

 

예전에 읽었을 때는 못 느꼈지만 이번에 읽어보니 토비 님께서 DI(Dependency Injection)를 설명하기 전에 DI가 왜 필요한지 직접 코드로 보여주셔서 DI라는 개념을 더 쉽게 이해할 수 있었습니다.

 

  • 관심사의 분리 : 중복 코드 메서드 추출
  • DB Connection 코드 독립 : 상속을 통한 확장 (is-a)
  • DAO의 확장 : 포함 관계를 통한 확장 (has-a)
  • 인터페이스 도입
  • 관계 설정 책임의 분리 - 인터페이스 구현체 주입을 외부로 분리

 

관심사의 분리

관심이 같은 것 끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것입니다.

 

상속을 통한 확장 (is-a)

public abstract class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
    ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
    ...
    }

    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

public class NUserDao extends UserDao {
    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // N 사 DB Connection 코드
    }
}

public class DUserDao extends UserDao {
    @Override
    public Connection getConnection() throws ClassNotFoundException, SQLException  {
        // D 사 DB Connection 코드
    }
}

위와 같이 상속을 통해 확장하게 될 경우 Java의 경우 다중 상속이 되지 않기 때문에 이 방식은 좋은 방법이라고 할 수 없습니다.

 

포함 관계를 통한 확장 (has-a)

public class SimpleConnectionMaker {

    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        return DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/springbook", "root", "1234"
        );
    }

}

public class UserDao {

    private final SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        this.simpleConnectionMaker = new SimpleConnectionMaker();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
    ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
    ...
    }

}

포함 관계의 경우에는 UserDao가 실제 SimpleConnectionMaker의 구현체를 알고 있기 때문에 이 역시 좋은 방법은 아닙니다.

 

인터페이스 도입

public interface ConnectionMaker {
    Connection makeConnection() throws ClassNotFoundException, SQLException;
}

public class DConnectionMaker implements ConnectionMaker {
    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // D 사 DB Connection 코드
    }
}

public class CConnectionMaker implements ConnectionMaker {
    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // C 사 DB Connection 코드
    }
}

public class UserDao {

    private final ConnectionMaker connectionMaker;

    public UserDao() {
        this.connectionMaker = new DConnectionMaker();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
    ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
    ...
    }

}

인터페이스를 작성하여 느슨한 연관 관계를 의도했지만, 아직 UserDao가 실제 구현체 정보인 DConnectionMaker를 알고 있습니다.

 

관계 설정 책임의 분리

public class UserDao {

    private final ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker) {
        // 외부에서 전달 받아 UserDao 에서 실제 구현체 정보를 가지고 있지 않게 됨
        this.connectionMaker = connectionMaker;
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
    ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
    ...
    }

}

public static void main(String[] args) throws SQLException, ClassNotFoundException {
	// 사용하는 쪽(client)에서 구현체 정보를 전달하여 관계설정 책임 분리
    UserDao dao = new UserDao(new DConnectionMaker());
    User user = new User();
}

이제 사용하는 쪽에서 실제 구현체 정보를 전달하도록 하여 UserDao에는 실제 구현체 정보를 가지지 않도록 했습니다.

 

IoC (제어의 역전)

IoC는 스프링을 통해 많이 알려진 용어이지만, 사실 알고 보면 스프링 이전 90년 중반에 출판된 GoF의 디자인 패턴 책에서 이미 이 용어를 사용했었다고 합니다.

 

애플리케이션 코드 간 의존성은 최대한 느슨하게 하여 영향이 없도록 하고, 실제 구현체 정보를 알 수 없도록 함  -> DI

DI를 개발자가 제어하는 것이 아닌 스프링(프레임워크)이 제어 → IoC 

 

라이브러리 vs 프레임워크

라이브러리: 개발자가 코드를 사용해 애플리케이션 흐름을 직접 제어함.

프레임워크: 프레임워크가 애플리케이션 흐름을 제어하고 개발자는 그 흐름에 맞춰 애플리케이션 코드를 작성함.

 

즉, 라이브러리냐 프레임워크냐의 기준은 코드의 제어권이 누구에게 있는지로 구분할 수 있습니다.

 

객체지향 설계 원칙(SOLID)

  • SRP(The Single Responsibility Principle): 단일 책임 원칙
  • OCP(The Open Closed Principle): 개방 폐쇄 원칙
  • LSP(The Liskov Substitution Principle): 리스 코프 치환 원칙
  • ISP(The Interface Segregation Principle): 인터페이스 분리 원칙
  • DIP(The Dependency Inversion Principle): 의존관계 역전 원칙

 

응집도

변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 할 수 있습니다.

 

결합도

변경이 일어날 때 관계를 맺고 있는 다른 오브젝트의 변경 정도라고 할 수 있으며, 변경이 일어날 때 다른 오브젝트의 변경 정도가 최소라면 결합도가 낮다고 할 수 있습니다.

 

객체지향에서는 응집도는 높게, 결합도는 낮게 설계하는 것이 바람직합니다.

 

오브젝트 팩토리

public class DaoFactory {

    public UserDao userDao() {
        return new UserDao(new DConnectionMaker());
    }

    public AccountDao accountDao() {
        return new AccountDao(new DConnectionMaker());
    }

    public MessageDao messageDao() {
        return new MessageDao(new DConnectionMaker());
    }

    // new DConnectionMaker() 생성 코드가 중복
}

-->

public class DaoFactory {

    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

    public AccountDao accountDao() {
        return new AccountDao(connectionMaker());
    }

    public MessageDao messageDao() {
        return new MessageDao(connectionMaker());
    }

    // ConnectionMaker 생성 중복 코드를 메소드로 분리
    public ConnectionMaker connectionMaker() {
        return new DConnectionMaker();
    }

}

앞의 관계설정 책임의 분리에서 사용하는 쪽에서 직접 구현체를 생성 후 제공했는데, 이제 사용하는 쪽에서 팩토리 객체에 userDao를 요청하기만 하면 됩니다.

 

// ConnectionMaker 생성 책임을 DaoFactory 로 위임
UserDao dao = new DaoFactory().userDao();

 

이제 ConnectionMaker 객체의 생성 책임을 팩토리 객체에 위임하여 사용자 쪽에서 실제 구현체 정보를 알고 있을 필요가 없게 되었습니다.

 

애플리케이션 콘텍스트에서 DaoFactory 사용

 

@Configuration
public class DaoFactory {

    @Bean
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }
    
    public ConnectionMaker connectionMaker() {
        return new DConnectionMaker();
    }

}

 

스프링 3.1 버전부터 지원하는 @Configuration 어노테이션을 사용하여 UserDao를 @Bean으로 등록하게 되면 ApplicationContext 객체에서 UserDao 빈을 조회하여 사용할 수 있게 됩니다.

 

// DaoFactory Bean 으로 생성
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);

 

 

애플리케이션 컨텍스트 동작 방식

사용자가 ApplicationContext에게 getBean으로 원하는 Bean을 호출하게 되면 ApplicationContext 가 Bean 목록에서 요청한 이름이 있는지 찾고, 있을 경우 빈 생성 메서드를 호출해 오브젝트를 생성하여 클라이언트에게 돌려줍니다.

 

Bean 등록 시 장점

위와 같이 UserDao를 Bean으로 등록하게 되면 이제 클라이언트는 구체적인 클래스를 알 필요가 없게 됩니다.

또한 ApplicationContext에서 오브젝트를 다루는 다양한 기능과 방식을 지원하게 됩니다.

 

싱글톤

DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();

System.out.println("dao1 : " + dao1);
System.out.println("dao2 : " + dao2);

ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao3 = context.getBean("userDao", UserDao.class);
UserDao dao4 = context.getBean("userDao", UserDao.class);

System.out.println("dao3 : " + dao3);
System.out.println("dao4 : " + dao4);


[출력 결과]
dao1 : com.tobyspring.studytobyspring.dao.UserDao@3967e60c
dao2 : com.tobyspring.studytobyspring.dao.UserDao@60d8c9b7

dao3 : com.tobyspring.studytobyspring.dao.UserDao@1869fbd2
dao4 : com.tobyspring.studytobyspring.dao.UserDao@1869fbd2

위와 같이 직접 객체를 생성하게 되면 매번 새 오브젝트가 만들어지기 때문에 두 객체는 서로 다릅니다.

하지만 Bean으로 등록하게 되면 기본적으로 싱글톤 스코프로 만들어지기 때문에 매번 조회하여도 같은 객체가 반환되어 두 객체는 서로 같습니다.

 

의존관계 주입

의존관계 주입이란 아래 3가지 조건을 만족할 경우 의존관계 주입이라고 합니다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않습니다. 그러기 위해서는 인터페이스에만 의존하고 있어야 합니다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정합니다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어집니다.

 

의존관계 검색과 주입

의존관계 검색(dependency lookup)은 객체 스스로가 컨테이너에게 bean을 요청하는 방법을 말합니다.

public class UserDao {

    private final ConnectionMaker connectionMaker;

    public UserDao() {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
    }
}

이 방법은 사용을 권장하지는 않지만 특수한 상황에 한정하여 사용해야 한다고 합니다.

특수한 상황이란, UserDao가 Bean으로 등록되지 못하는 상황에서 UserDao가 포함하고 있는 ConnectionMaker 객체를 DI 받아야 하는 상황을 의미합니다.

 

 

추가로 공부해봐야 할 부분

이번 토비의 스프링 읽기 모임 시간에 많이 느꼈던 부분인데,

사실 Bean을 생성할 때 기본 스코프인 싱글톤만 사용하다 보니 스프링이 Bean을 생성할 때 기본으로 싱글톤 스코프로 만든다. 까지만 학습하고 빈 스코프, 또는 싱글톤 레지스트리에 대한 학습이 부족하다고 느꼈습니다.

 

그리고 스프링이 자바를 사용하다 보니 자연스럽게 디자인 패턴에 대한 내용이 자주 등장하는데, 디자인 패턴 역시 학습을 제대로 해보지 않아 이번 기회에 이 부분들을 추가로 학습하고 정리해야겠다고 느꼈습니다.

추후 해당 부분들에 대해 학습한 후 정리하여 링크를 남겨야겠습니다.

 

  • 빈 스코프
  • 싱글톤 레지스트리
  • 디자인 패턴

'Spring > Toby's Spring Reading Club' 카테고리의 다른 글

1-5장. 서비스 추상화  (0) 2023.01.08
1-4장. 예외  (0) 2023.01.08
1-3장. 템플릿  (0) 2023.01.07
1-2장. 테스트  (0) 2022.09.19
토비의 스프링 3.1 시작하기  (0) 2022.09.04

댓글