본문 바로가기

Spring/토비의스프링

오브젝트와 의존관계

오브젝트와 의존관계

스프링은 자바를 기반으로한 기술이고

스프링이 자바에서 가장 중요하게 여기는건 객체지향이 가능한 프로그래밍 언어라는 점이여서 

오브젝트에 가장 많은 관심을 가지고 있다.

 

따라서 스프링을 이해하기 위해서는 오브젝트에 대해 알고있어야 한다.

(오브젝트의 관심은 오브젝트 설계의 관심까지 발전하게 되는데 객체지향 설계의 기초와 원칙을 비롯해서

디자인패턴, 단위테스트 같은 오브젝트 설계와 구현에 관심을 갖게 된다.)

 

스프링은 오브젝트를 어떻게 효과적으로 설계, 사용할지 기준을 제공해줘서

객체지향 기술과 설계 구현에 관한 실용적인 전략과 검증된 베스트프랙티스를 손쉽게 제공해주는 프레임워크다.

 

1장에서는 오브젝트의 설계와 구현, 동작원리에 더 집중을 해서 알아보자

 

초난감 DAO

@Getter
@Setter
@NoArgsConstructor
public class User {
    private String id;
    private String name;
    private String password;


    @Builder
    public User(String id, String name, String password) {
        this.id = id;
        this.name = name;
        this.password = password;
    }


}

 

정보에 접근하는 UserDao

 

public class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("org.h2.Driver");
        Connection c = DriverManager.getConnection();

        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values (?, ?, ?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Class.forName("org.h2.Driver");
        Connection c = DriverManager.getConnection();

        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();

        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
}

 

이 코드의 문제점이 뭔지 알아보고 객체지향적으로 만들어가는 방법을 알아보자

 

DAO 분리

코드는 항상 변화에 대비해야 한다.

절차적 프로그래밍에 비해

객체지향은 변화에 효과적으로 대처할 수 있다는 기술적인 특징이 있다.

그러면 어떻게 변경이 일어났을때, 작업을 최소화하고 다른곳에 문제를 일으키지 않게 할 수 있을까

분리와 확장을 고려해서 설계를 하면된다.

 

커넥션만들기의 추출

먼저 코드를 보면 중복된 부분이 많은걸 볼 수 있다.

 

  • DB와 연결을 위해 커넥션을 가져오는 부분
  • Statement 를 만들고 실행하는 부분들
  • 작업이 끝나고 리소스를 종료해주는 부분

 

커넥션을 가져오는 부분먼저 제거해보자

커넥션을 가져온다는 관심사가 다른 관심사들과 섞여있고, 메소드마다 중복 되어있다.

getConnection 메소드를 만들어서 연결이 필요할때마다 이 메소드를 호출하게 변경해본다.

 

private Connection getConnection() throws ClassNotFoundException,SQLException{
  class.forName("com.mysql.jdbc.Driver");
  Connection C = DriverManager.getConnection();
  return c
}

이렇게 빼두면 데이터베이스 종류가 변경되더라도

한 메소드에서만 url 등을 바꾸면 전체가 변경되도록 적용 할 수 있다.

 

DB커넥션 만들기의 독립

근데 UserDao 를 다른 회사에 납품을 하게 되었다고 가정하자.
하지만 add 나 get 같은 로직은 공개하지 않고 클래스 바이너리 파일만 제공을 하고 싶다.
거기다가 db 커넥션을 맺는 getConnection은 납품처마다 연결 방식을 다르게 하고 싶어한다.
이런 경우엔 어떻게 제공을 해야 할까

 

좀 더 변화를 줘서 상속을 통한 확장을 봐보자

 

UserDao 를 추상클래스로 선언하면 좀 더 확장성 있는 소스가 될 수 있다.

add(), get() 만 구현을 해두고 getConnection() 메소드를 추상메소드로 선언해두면

상속받는 클래스에서 getConnection을 구현해서 서로 다른 DB에 접근하게 만들 수 있다.

 

이렇게 슈퍼클래스는 기본적인 로직을 만들고 서브클래스에서 메소드를 구현하는 패턴을 템플릿 메소드 패턴이라고 한다.

스프링에서 애용하는 패턴이다.

 

그리고 UserDao 서브 클래스의 getConnection 메소드는 어떤 Connection 클래스의 객체를 생성할지 정하게 되는데,

이렇게 서브클래스에서 어떤 객체를 생성할지 정하는 방식을 팩토리 메소드 패턴이라고 한다.

팩토리 메소드 패턴을 사용하면 UserDao 에서는 그냥 Connection 타입의 인터페이스를 사용했다는것만 관심을 갖고

실제 구현은 그 밑에 서브클래스에서 만들어낼 수 있다.

깔끔하게 관심사항을 분리 할 수 있다. 

 

 

DAO 의 확장

 

위에 처럼 구현을 하더라도 상속을 사용하기 때문에 지독한 의존성을 가지게 된다.

서브클래스는 슈퍼 클래스의 기능들을 사용 할 수 있고 슈퍼클래스의 변경이 생기면 

서브 클래스도 같이 수정해야 될 수도 있다. 

이 의존성을 좀 더 느슨하게 만들어 보자

 

클래스의분리

DB 커넥션과 관련된 부분을 서브클래스가 아니라, 아예 별도의 클래스로 분리해보자.

클래스를 만들어서 add, get 에서는 SimpleConnectionMaker의 makeNewConnection()을 사용해서 상속을 없앨 순 있엇지만, N사나 D 사에 클래스를 제공하는게 불가능 해졌다.

new SimpleConnectionMaker 부분을 변경시켜야 되기 떄문에

 

클래스를 분리했을때도 자유로운 확장이 가능하려면 두가지 문제를 해결해야한다.

 

1.  N사나 D사에서 커넥션을 만드는 클래스에서는 메소드명이 다르다면 

모든 add, get 등 메소드에서 사용되는 메소드 명이 바껴야한다.

 

2. DB 커넥션을 제공하는 클래스가 어떤것인지 UserDao 가 정확히 알아야한다. new SimpleConnectionMaker 처럼 클래스타입의 변수까지 정의해놓기 때문에, N 사에서 다른 클래스를 사용하면 UserDao 코드가 변경되어야 한다.

 

이래서는 상속을 이용한것보다 못한 방법이 된다.

 

인터페이스의 도입

클래스를 분리해도 해당 클래스를 생성시키는 의존성을 갖게 된다.

서로 느슨하게 연결 할 수 있게 추상화시키는 인터페이스를 사용 할 수 있다.

 

추상화란 공통적인 성격을 뽑아내서 따로 분리하는 작업이다.

인터페이스를 도입해서 의존성을 낮춰줬다.

인터페이스를 사용하면 실제 구현 클래스는 신경 쓸 일이 없다.

DB 커넥션을 가져오는 메소드는 makeConnection 이라 정해 뒀기 때문에 

UserDao 입장에서는 어떤 클래스로 만들어졌든지 makeConnection 메소드를 호출하기만 하면된다.

 

근데 이렇게 인터페이스를 도입해서 커넥션을 생성시킬때

ConnectionMaker 를 생성시켜줘야하는 의존성이 생기게 된다.

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

이런 의존성을 없애주려면 호출하는 부분에서 의존성을 넣어주도록 변경해주면 된다.

객체지향 프로그래밍에는 다형성이라는 특징이 있기 때문에

클래스의 오브젝트를 인터페이스 타입으로 받아서 사용 할 수 있다.

실제 사용되는 클래스는 런타임시에 넣어줄 수 있다.

 

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

public class DConnectionMaker implements ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
    }
}
public class UserDao {
    private ConnectionMaker connectionMaker; 

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

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
    }

    public user get(String id) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
    }
}

인터페이스를 이용해서 상속을 통한 확장 보다 더 깔끔하고 유연한 방법으로 UserDao 와 ConnectionMaker 클래스를 분리하고 서로 영향을 주지 않고 자유롭게 확장할 수 있는 구조가 됐다

 

원칙과 패턴

객체지향 기술의 이론을 통해 지금까지 해온 작업의 결과가 어떤 장점이 있는지 알아보자

 

 

개방 폐쇄원칙

 

클래스나 모듈이 확장에는 열려있고 변경에는 닫혀있어야한다.

UserDao 는 DB 연결 방법이라는 기능을 확장하는데는 열려있다.

UserDao 에 전혀 변경 없이 확장이 가능하다. 잘 설계된 클래스의 구조들은 대부분 이 개방 폐쇄 원칙을 잘 지키고 있다고 볼 수 있다.

인터페이스를 사용해 확장 기능을 사용한 대부분은 개방 폐쇄 원칙을 잘 따르고 있다고 볼 수 있다.

 

높은 응집도와 낮은 결합도

높은 응집도는 클래스가 하나의 책임 또는 관심사에 집중 되어 있다는 뜻이다.

낮은 결합도는 느슨하게 연결된 형태를 유지하는 것. 결합도가 낮아야 변화에 대응하는 속도가 높아진다.

 

전략패턴

 

이런 구조를 디자인패턴으로 보면 전략 패턴에 해당한다.

개방 폐쇄 원칙을 실현한 패턴이라고 볼 수 있다.

 

전략패턴은 자신의 기능에서 변경이 필요한 부분을 인터페이스로 외부로 분리 시키고

전략에 맞게 구현된 알고리즘 클래스를 필요에 따라 바꿔서 사용 할 수 있는 패턴이다.

전략을 바꿔가면서 사용 할 수 있다.

 

스프링은 지금까지 설명한 객체지향적 설계 원칙과 디자인 패턴에 나타난 장점들을 자연스럽게

개발자들이 활용할 수 있게 해주는 프레임워크다.

이제 이런 기술들이 사용된 스프링을 한번 봐보자 

 

제어의 역전(IOC)

제어의 역전이 뭔지 보기위해 UserDao 코드를 좀 더 개선해보자

 

오브젝트 팩토리

위에 코드에서는 UserDaoTest 에서 어떤 ConnectionMaker 를 사용할지 구현 클래스를 넣어주는 기능도 같이 했는데,

이렇게 사용을하면 Test 클래스에서 Test 뿐만 아니라 클래스를 구현시키는 다른 책임까지 맡고 있는 상황이 되버린다.

클래스를 구현시키는 부분은 따로 분리해보자

 

팩토리

분리시킬 기능을 할 클래스를 하나 만들어보자

팩토리 클래스의 역할은 객체의 생성방법을 결정하고 만들어진 객체를 돌려주는것

팩토리 클래스의 역할을 맡는 DaoFactory를 봐보자

 

Test에 담겨있던 UserDao, ConnectionMaker 생성 작업을 팩토리로 옮기고 

Test 에서는  팩토리에 요청해서 미리 만들어져 있는 객체를 사용하게 만든다. 

 

public class DaoFactory{
  public UserDao userDao(){
    ConnectionMaker connectionMaker = new DConnectionMaker();
    UserDao userDao = new UserDao(connectionMaker);

    return userDao;
  }
}

Factory 클래스를 통해서 어떤 커넥션메이커를 쓸지 정해서 해당 객체를 return 시킬 수 있다.

 

이런 구조를 갖게된다.

실질적인 로직은 담당하는 컴포넌트들과 이런 컴포넌트의 구조와 관계를 정의한 설계도 같은 역할을 하는 팩토리가 있다.

 

클라이언트를 Factory에 요청을 해서 사용할 UserDao 를 알맞게 준다.

 

 

팩토리 활용

package springbook.user.dao;

public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }
    public AccountDao userDao() {
        return new AccountDao(connectionMaker());
    }
    public MessageDao userDao() {
        return new MessageDao(connectionMaker());
    }
    public ConnectionMaker connectionMaker() {
        return new DConnectionMaker();
    }
}

반복되는 connectionMaker 가 있다면 공통 메소드로 빼서 사용 할 수 있다.

 

제어권의 이전을 통한 제어관계 역전

제어의 역전이라는건 프로그램의 제어 흐름 구조과 뒤바뀐것이라 할 수 있다.

 

보통의 프로그램 흐름은 사용할 오브젝트를 생성하고 만들어진 오브젝트를 사용한다.

모든 작업을 사용하는 쪽에서 제어하는 구조인데, 제어의 역전은 이 흐름을 뒤집는것.

 

제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다.

모든 제어 권한을 내가 아닌 다른 대상에 준다.

모든 오브젝트는 이렇게 위임 받은 제어 권한을 가진 특별한 오브젝트에 의해 결정된다.

 

* 제어의 역전은 폭넓게 사용되고 있다. 예를들면 서블릿 같이,

서블릿 안에는 main()이 있는게 아니라 직접 실행 시킬 수 없지만 서블릿에 제어 권한을 가진 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고 그 안에서 메소드를 호출한다.

 

* 위 예제로 보면 처음에는 ConnectionMaker 클래스를 결정하고 오브젝트를 만드는 권한이 

UserDao 에 있었지만 지금은 DaoFactory에 있다. 어떤 구현 클래스를 사용할지 권한을 DaoFactory에 넘겨서 수동적으로 만들어지게 된다. 이렇게 관심을 분리하고 책임을 나누고 유연하게 확장 가능하게 만들었던 구조가 

IoC 를 적용하는 작업이었다고 볼 수 있다.

 

이제 스프링의 IOC 에대해 본격적으로 봐보자

 

Spring IOC

 

스프링의 핵심은 빈팩토리, 또는 Application Context 가 담당하고 있다.

DaoFactory 의 일을 좀 더 일반화 시킨 것이라 볼 수 있다.

 

오브젝트 팩토리를 이용한 스프링 IOC

스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 객체를 빈이라고 한다.

빈의 생성과 관계설정등 제어를 담당하는 IOC 오브젝트를 빈팩토리라고 한다.

빈팩토리에서 좀 더 기능들을 확장한게 Application Context 다. 

 

어플리케이션 컨텍스트는 빈의 생성, 관계 설정등의 제어 작업들을 총괄한다.

그 자체로는 로직을 담당하진 않지만 컴포넌트 생성이나 관계를 맺어주는 책임을 담당한다.

 

DaoFactory 를 사용하는 ApplicationContext

ApplicationContext가 사용될 수 있게 설정 정보가 필요하다.

빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스에 @Configuration 어노테이션을 추가하고

객체로 만들어줄 클래스에는 @Bean 어노테이션을 추가한다.

 

@Bean 으로 관리되는 객체는 아래와 같이 꺼내 쓸 수 있다.

 

DaoFactory 를 설정정보로 사용하는 어플리케이션 컨텍스트에서 빈을 꺼내서 사용하고 있는 모습

public class UserDaoTest{
  public static void main(String[] args) throws ClassNotFoundException,SQLException{
    ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
    UserDao dao = context.getBean("userDao",UserDao.class);
    ...
  }

}

 

그런데 이렇게 DaoFactory에 @Configuration 을 달아서 사용하는건 기존에 만들었던 

DaoFactory를 직접 만들어서 사용한거랑 기능적으로 다를 바가 없다.

 

하지만 이렇게 사용하면 DaoFactory를 직접 만드는 것에서는 얻을 수 없는 방대한 기능과 활용 방법들을 제공 받을 수 있다. 이 특성들에 대해서는 앞으로 알아보자

 

ApplicationContext의 동작 방식

ApplicationContext를 IOC 컨테이너, 또는 스프링 컨테이너라 부르기도 한다.

@Configuration 클래스를 통해 설정을 하게 되고 저장된 @Bean 객체를 요청할때 전달해준다.

 

ApplicationContext를 사용할때의 장점

 

- 클라이언트가 구체적인 팩토리 클래스를 알 필요가 없다.

- ApplicationContext는 단순히 오브젝트 생성과 관계설정만 해주는게 아니라 인터셉터, 객체마다 설정 다르게 하기 등 여러 종합 서비스를 제공

- bean을 검색하는 다양한 방식을 제공한다.

 

싱글톤 레지스트리와 오브젝트 스코프

어플리케이션 컨텍스트를 이용하면 동일성이 보장되는 객체들이 출력된다.

 

싱글톤 레지스트리로서의 어플리케이션 컨텍스트

어플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리다.

별다른 설정 없이도 모든 빈 객체를 싱글톤으로 저장한다.

 

서버 애플리케이션 싱글톤

스프링이 싱글톤을 사용하는 이유는 사용되는 환경이 서버 하나당 초당 수십에서 수백 번씩 요청을 처리 해야하는 환경

에서 처리를 해야 했기 때문이다.

매 요청마다 새로운 객체를 만들게 되면 엄청 많은 객체가 생성되고 서버의 부하가 가게 된다.

엔터프라이즈에서의 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작한다.

 

하지만 싱글톤 패턴은 사용하기가 까다롭고 여러 문제를 가지고 있다.

 

싱글톤 패턴의 한계

싱글톤 코드를 추가하고 나면 코드가 상당히 지저분해지게 된다.

싱글톤 패턴 구현 방식에는 다음과 같은 문제가 있다.

 

  • private 생성자를 갖고있어서 상속이 불가능해진다.
  • 테스트 하기가 힘들다
  • 서버에서 클래스로더를 어떻게 구성하냐에 따라 싱글톤임에도 하나 이상의 오브젝트가 만들어 질 수 있다.
  • 싱글톤은 스태틱 메소드를 이용해서 언제든지 접근 할 수 있기 때문에 누구나 사용가능한 전역상태가 되버린다.

 

싱글톤 레지스트리

싱글톤 패턴의 구현 방식이 여러 단점이 있기 때문에 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 그게 바로 싱글톤 레지스트리

 

싱글톤 레지스트리의 장점은 스태틱 메소드나 private 생성자를 사용하지 않아도 평범한 자바 클래스도 

싱글톤 방식으로 관리 되게 한다.

이 덕분에 싱글톤임에도 public으로 사용이 가능하다.

또한 싱글톤임에도 public 생성자를 가질 수 있어서 테스트 코드에서는 간단한 오브젝트를 생성해서 테스트 해 볼 수 있다.

 

의존관계 주입(DI)

제어의 역전과 의존관계 주입

스프링을 IOC 컨테이너라고만 하면 스프링이 제공하는 기능의 특징을 명확하게 말하지 못한다.

그래서 나온말이 IOC 방식의 핵심을 설명하는 의존관계 주입(DI) 다.

스프링의 IOC 대표 동작 원리는 주로 의존관계 주입이라고 불린다.

 

런타임 의존관계 설정

의존관계

의존관계는 항상 방향성이 부여된다. 누가 누구에게 의존하는 관계다. 

 

 

이 그림에선 UserDao 가 ConnectionMaker 에 의존하고 있다.

이렇게 인터페이스에만 의존성을 만들어두면 구현 클래스와의 관계는 느슨해지면서 변경에 자유로워진다.

근데 모델이나 코드에서 적는 의존관계 말고도 런타임시에 의존관계가 만들어지기도 한다.

UserDao 는 ConnectionMaker 인터페이스를 통해서만 느슨한 의존관계를 갖고

런타임시에 어떤 클래스와 맺어지는지 미리 알 수 없고 

UserDao 와 ConnectionMaker 인터페이스를 구현한 클래스의 오브젝트가 런타임시에 맺어지게 된다

여기서 DConnectionMaker 와 맺어지고 이걸 의존 오브젝트라 한다.

 

의존관계 주입은 런타임 시에 의존 오브젝트를 클라이언트에 연결해주는 작업이다.

 

의존관계 주입의 조건

  • 코드에는 의존관계가 드러나지 않고 인터페이스에만 의존하고 있다.
  • 런타임 시점에 의존관계는 팩토리 같은 제 3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.

의존관계 주입의 핵심은 두 오브젝트의 관계를 맺게 도와주는 제 3의 존재가 있다는,

관계설정 책임을 가진 클래스가 있다는 말이다.

 

 

 

'Spring > 토비의스프링' 카테고리의 다른 글

AOP  (0) 2021.09.13
서비스 추상화  (0) 2021.09.10
예외  (0) 2021.09.08
템플릿  (0) 2021.09.07
테스트  (0) 2021.09.07