티스토리 뷰

1장 오브젝트와 의존관계

스프링이 자바에서 가장 중요하게 가치를 두는 것은 바로 객체지향 프로그래밍이 가능한 언어라는 점이다. 자바 엔터프라이즈 기술의 혼란 속에서 잃어버렸던 객체지향 기술의 진정한 가치를 회복시키고, 그로부터 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가자는 것이 스프링의 핵심 철학이다.

그래서 스프링이 가장 관심을많이 두는 대상은 오브젝트다. 스프링을 이해하려면 먼저 오브젝트에 깊은 관심을 가져야 한다.

 

1.1 초난감 DAO

DAO : DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트를 말한다.

1.1.2에서 만든 UserDao 라는 클래스에는 여러 가지 문제가 있다. 이 초난감 DAO 코드를 객체지향기술의 원리에 충실한 코드로 개선할 것이다.

UserDao 클래스 코드의 문제점은 무엇인지 생각해보자.

  1. 정상적으로 동작하는 이 코드에 문제가 많다고 하는 것일까?
  2. 잘 동작하는 코드를 굳이 수정하고 개선해야 하는 이유는 뭘까?
  3. 그렇게 DAO 코드를 개선했을 떄의 장점은 무엇일까?
  4. 그런 장점들이, 또는 미래에 주는 유익은 무엇인가?
  5. 또, 객체지향 설계의 원칙과는 무슨 상관이 있을까?
  6. 이 DAO를 개선하는 경우와 그대로 사용하는 경우, 스프링을 사용하는 개발에서 무슨 차이가 있을까?

스프링을 공부한다는 것은 이런 문제 제기와 의문에 대한 답을 찾아나가는 과정이다.
스프링은 이러한 질문에 대한 답변을 주지 않는다. 이 답변은 개발자 스스로 만들어 내는 것이지 스프링이 덥석 줄 수 있는게 아니기 때문이다.

 

1.2 DAO의 분리

관심사의 분리

변화는 언제든지 일어날 수 있다. 객체지향 설계와 프로그래밍이 이전의 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 많은, 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다.

 

객체지향 기술은 흔히 실세계를 최대한 가깝게 모델링해낼 수 있기 때문에 의미가 있다고 여겨진다. 하지만 그보다는 객체지향 기술이 만들어내는 가상의 추상세계 자체를 효과적으로 구성할 수 있고, 이를 자유롭고 편리하게 변경, 발전, 확장시킬 수 있다는데 더 의미가 있다.

 

미래를 준비하는데 가장 좋은 방법은 변화의 폭을 최소한으로 줄여주는 것이다. 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 하는 것이다.

 

그렇다면 어떻게 위와 같이 문제를 일으키지 않게 할 수 있을까 ?

→ 그것은 분리와 확장을 고려한 설계가 있었기 때문이다.

 

변화가 한 번에 한 가지 관심에 집중돼서 일어난다면, 우리가 준비해야 할 일은 한 가지 관심이 한 군데에 집중되게 하는 것이다. 즉 관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것이다.

 

프로그래밍의 기초 개념 중에 관심사의 분리라는 게 있다. 이를 객체지향에 적용해보면, 위와같이 서로 영향을 주지 않도록 분리하는 것이라고 생각할 수 있다.

 

1. 중복 코드의 메소드 추출

private Connection getConnection() throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.jdbc.Driver");
    Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8", "spring", "book");
    return c;
}

위와 같이 중복된 코드를 getConnection()이라는 이름으로 분리하였다.
이 작업은 기능에는 영향을 주지 않으면서 코드의 구조만 변경한다. 미래의 변화에 좀 더 손쉽게 대응할 수 있는 코드가 됐다.
이런 작업을 리팩토링 이라고 한다.
또한 위에서 getConnection()이라고 하는 공통의 기능을 담당하는 메소드로 중복된 코드를 뽑아내는 것을 리팩토링에서는 메소드 추출기법이라고 한다.

 

2.상속을 통한 확장

getConnect()를 추상메서드로 만들어 다른 슈퍼클래스와 서브클래스로 분리한다.

템플릿 메소드 패턴

이렇게 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브 클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 디자인 패턴

팩토리 메소드 패턴

서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 디자인 패턴

 

1.3 DAO의 확장

  1. 클래스 분리
  2. 상속으로 분리를 한다면 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없는 것도 큰 단점이다.
    만약 UserDao 외의 DAO 클래스들이 계속 만들어진다면 그때는 상속을 통해서 만들어진 getConnection()의 구현 코드가 매 DAO 클래스마다 중복되는 현상이 나타날 것이다.
    따라서 완전히 독립적인 클래스로 만들어 분리시키자.
  3. 인터페이스 도입인터페이스를 이용해 DB 커넥션을 제공하는 클래스에 대한 구체적인 정보는 모두 제거가 가능했지만,
    결국 어떤 클래스의 오브젝트를 사용할 것인지를 결정하는 생성자의 코드는 제거되지 않고 남아 있다는 문제가 있다.
  4. 클래스를 분리하면 특정 클래스와 그 코드에 종속적이기 때문에 자유롭게 확장하기가 힘들다.
    가장 좋은 해결책은 두 클래스 사이에 느슨한 연결고리(인터페이스)를 만들어 주는 것이다.
    자신을 구현한 클래스에 대해 구체적인 정보를 모두 감춰버리기 떄문에 인터페이스로 추상화해놓은 최소한의 통로를 통해
    접근하는 쪽에서는 오브젝트를 만들 때 사용할 클래스가 무엇인지 몰라도 된다. 따라서 클래스를 바꿔도 신경 쓸 일이 없다.
  5. 관계설정 책임의 분리
  6. 위와 같은 문제가 발생하는 이유는 UserDao 안에 분리되지 않은, 또 다른 관심사항이 존재하기 때문이다.
    이 문제에 대한 해결책은 오브젝트와 오브젝트 사이의 관계를 설정해주어야 한다.

원칙과 패턴

개방 폐쇄 원칙 - OCP (Open-Closed Principle)

OCP란 '클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.'라고 할 수 있다.
우리가 지금까지 리팩토링한 UserDao 클래스는 이제 기능을 확장하는데는 열려있고 변경에는 닫혀 있다고 말할 수 있다.

높은 응집도와 낮은 결합도

개방 폐쇄 원칙은 높은 응집도와 낮은 결합도라는 소프트웨어개발의 고전적인 원리로도 설명이 가능하다.
응집도가 높다는 건 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다.

높은 응집도란 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명할 수 있다.
낮은 결합도란 느슨하게 연결된 형태를 유지하는 것을 말한다.

전략 패턴

자신의 기능 맥락에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고,
이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.

 

1.4 제어의 역전 (IoC)

제어의 역전(IoC)이라는 것은 간단히 프로그램의 제어 흐름 구조가 뒤바뀌는 것이라고 설명할 수 있다.
즉, 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 말한다.

 

팩토리

객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 역할을 하는 것을 팩토리라고 부른다.
팩토리는 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 깔끔하게 분리하려는 목적으로 사용하려는 것이다.

 

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

모든 오브젝트가 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들지를 스스로 관장한다.
모든 종류의 작업을 사용하는 쪽에서 제어하는 구조다

제어의 역전이란 이런 제어 흐름의 개념을 거꾸로 뒤집는 것이다. 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다.
당연히 생성하지도 않는다. 또 자신도 어떻게 만들어지고 어디서 사용되는지를 알 수 없다.
모든 제어 권한을 다른 대상에게 위임하기 때문이다.
프로그램의 시작을 담당하는 main()과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다.

 

IoC를 적용함으로서 생기는 장점
  1. 설계가 깔끔해진다.
  2. 유연성이 증가한다.
  3. 확장성이 증가한다.

스프링은 IoC를 모든 기능의 기초가 되는 기반으로 삼고 있으며, IoC를 극한까지 적용하고 있는 프레임워크다.

 

1.5 스프링의 IoC

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

스프링 빈 (Bean)

  • 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트
  • 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트
  • IoC를 당하는 주체

빈 팩토리 (Bean Factory)

  • 빈의 생성과 관계설정 등 제어를 담당하는 IoC 오브젝트 (컨테이너)
  • 빈을 생성하고 관계를 설정하는 IoC의 기본 기능을 상징
  • IoC를 하는 주체

애플리케이션 컨텍스트 (Application Context)

  • IoC방식을 따라 만들어진 빈 팩토리의 일종
  • 빈 팩토리를 확장한 IoC 컨테이너
  • 빈 팩토리 기능 + 스프링이 지원하는 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진
  • 설정정보를 참고해서 빈의 생성, 관계 설정 등의 제어작업을 총괄

설정정보 / 설정 메타정보 (Configuration)

  • 애플리케이션 컨텍스트 / 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보
  • IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용
  • 설정정보 클래스에는 @Configuration을 추가해주면된다.
  • 건물이 설계도면을 따라 만들어지듯, 애플리케이션도 애플리케이션 컨텍스트와 그 설정정보를 따라서 만들어지고 구성되는 것

컨테이너

  • IoC방식으로 빈을 관리하는 빈 팩토리, 애플리케이션 컨텍스트를 지칭
@Configuration // 설정 정보로써 애플리케이션 컨텍스트 or 빈 팩토리가 사용하라는 의미
public class DaoFactory {
    @Bean // 오프젝트의 생성을 담당해하는 IoC용 메소드 임을 표시
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }
    @Bean
    public ConnectionMaker connectionMaker() {
        return new DConnectionMaker();
    }
}

 

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

ApplicationContext ac = new AnnotationConfigApplicationContext(DaoFactory.class);

UserDao dao1 = ac.getBean("userDao", UserDao.class);
UserDao dao2 = ac.getBean("userDao", UserDao.class);
  • getBean() : 스프링 빈 컨테이너 내부 테이블에서 등록되어있는 빈을 가져온다.
    • 첫번째 매개변수는 메소드 명, 두번째 매개변수는 반환할 타입 (캐스팅 용도)

위 코드를 입력하면 dao1와 dao2 객체가 생성될텐데 두 객체가 동일할까?

→ 동일하다

  • 애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이기 때문

싱글톤 패턴

어떤 클래스를 애플리케이션 내에서 제한된 인스턴스 개수, 즉 하나만 존재하도록 강제하는 패턴이다. 이렇게 하나만 만들어지는 클래스 오브젝트는 애플리케이션 전역으로 접근이 가능하다.

단일 오브젝트로만 존재해야되고 애플리케이션의 다양한 곳에서 공유해야하는 경우 주로 사용한다.

public class UserDao {
    private static UserDao INSTANCE;
    ...
    private UserDao (ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }

    public static synchronized UserDao getInstance() {
        if (INSTANCE == null) INSTANCE = new UserDao(/*인자*/);
        return INSTANCE;
    }
}

단점

  • private 생성자로 인해 상속이 제한된다.
  • 싱글톤은 테스트하기가 힘들다
  • 복수의 JVM에 분산되어 서버가 설치되면 독립적으로 생성된다
  • 전역상태로 사용되기 쉽다.

스프링은 싱글톤이 서비스 오브젝트 방식으로 사용되는 것을 적극 지지하는데, 위의 단점으로 스프링이 직접 싱글톤 형태의 오브젝트를 만들고 관리한다.

→ 그게 바로 싱글톤 레지스트리 Singleton Registry

  • 일반적인 클래스에 @Bean만 달아주면 싱글톤으로 관리해준다.

싱글톤 객체는 무상태로 만들어야 되는데 그 이유는 멀티 쓰레드 환경에서 다수의 사용자의 요청을 받게 되면 동시에 싱글톤 객체에 접근해서 값을 서로 덮어쓰려고 하기 때문이다. (단, 읽기전용 값인 경우 상관없다.)

→ 인스턴스 변수를 갖게하면 안된다.

  • 싱글톤은 인스턴스에 값을 갖게하고 변경하는 상태유지 (stateful) 방식으로 만들지 않는다.

상태없이 요청에 대한 정보나, DB로부터 받은 정보는 어떻게 다룰까?

→ 파라미터, 로컬변수, 리턴값으로 활용하면 된다.

  • 매소드 파라미터, 메소드 내에서 생성되는 로컬 변수는 쓰레드 별로 갖는 스택에 저장되므로 여러 쓰레드가 공유하지 않아서 덮어쓸일이 없다.

빈 스코프

스코프 - 빈이 생성되고 존재하고 적용되는 범위

  • 싱글톤 스코프 : 스프링 컨테이너가 존재하는 동안 계소 유지 (기본값)
  • 프로토 타입 스코프 : 싱글톤과 달리 빈을 요청할 때마다 새로운 오브젝트를 생성
  • request 스코프 : HTTP 요청이 생길 때 마다 생성
  • 세션 스코프 : 웹의 세션과 스코프가 유사하다

 

1.7 의존관계 주입 (DI)

DI : dependency injection

의존관계란?

  • [A] → [B] : A가 B에 의존하고 있다.
  • 의존한다 : B의 변화에 A가 영향을 받는다.

UserDao 의존관계

  • UserDao는 인터페이스 ConnectionMaker에 의존한다.
  • DConnectionMaker는 인터페이스 ConnectionMake를 구현한다.

위 구조는 클래스 UserDao가 ConnectionMaker 인터페이스를 통해서 구현클래스와 사용 클래스간의 결합도가 낮도록, 느슨한 관계를 갖도록 했다.

DConnectionMaker의 구현 코드가 변경되어도 UserDao는 영향받지 않는다. 인터페이스가 변경되어야 영향을 받게된다.

의존관계 주입

  1. 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다.
    • 그러기 위해 인터페이스에만 의존해야한다.
    • 내가 사용할 인터페이스의 구현 클래스가 무엇인지는 런타임 시점에 알게 되는 것이다.
      (다형성이 용이해진다)
  2. 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
  3. 의존관계는 사용할 오브젝트에 대한 레퍼런스(참조)를 외부에서 제공(주입)해줌으로써 만들어진다.
public class UserDao {
    private ConnectionMaker connectionMaker;
    ...
    // DI - 의존관계 주입
    public UserDao (ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}
  • UserDao는 connectionMaker를 사용할 뿐 인터페이스의 구현체가 먼지 알지도 궁금하지도 않다.

의존관계 검색 방법

public class UserDao {
    private ConnectionMaker connectionMaker;
    ...
    // 의존관계 검색
    DaoFactory daoFactory = new DaoFactory();
    this.connectionMaker = daoFactory.connectionMaker();
}

---
// DaoFactory 클래스
@Bean
public ConnectionMaker connectionMaker() {
    return new ProductionDBConnectionMaker();
}

의존관계 주입 방법

수정자 (Setter) 메소드를 이용한 주입

수정자 메소드는 외부에서 오브젝트 내부의 애트리뷰트값을 변경하려는 용도로 사용된다.

일반 메소드를 이용한 주입

여러개의 파라미터를 한번에 갖는 일반 메소드를 DI용으로 사용할 수 있다.

수정자를 사용해서 DI를 받는 UserDao 클래스

public class UserDao {
    private ConnectionMaker connectionMaker;
    ...
    // DI 받기
    public void **setConnectionMaker**(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }

    //CRUD
}

수정자 메소드 DI를 사용하는 팩토리 메소드 UserFactory 수정

@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    userDao.**setConnectionMaker**(connectionMaker());
    return userDao;
}

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

@Bean
public ConnectionMaker realConnectionMaker() {
    return new DConnectionMaker(); // 실제 구현 클래스
}

1.8 XML을 이용한 설정

-생략-

 

1장 결론

관심사의 분리, 리팩토리

책임이 다른 코드를 분리해서 별도의 클래스로 만든다

전략패턴

그 중에서 변경 가능성이 있는 클래스는 인터페이스를 구현하도록 하고, 다른 클래스는 인터페이스에 의존하도록 했다. 이렇게 함으로써 인터페이스 구현 클래스가 다른 클래스로 변경되어도 인터페이스에 의존하는 클래스는 영향을 받지 않게 된다.

개방 폐쇄 원칙 OCP

자신의 책임 자체가 변경되는 경우 외에는 불필요한 변화가 발생하지 않도로 막아두고, 자신이 사용하는 외부 오브젝트의 기능은 자유롭게 확장하거나 변경할 수 있게 만든다.

결국 한쪽의 기능변화가 다른 쪽의 변경을 요구하지 않아도 되고 (낮은 결합도), 자신의 책임과 관심사에만 순수하게 집중하는 (높은 응집도) 코드를 만들 수 있다.

제어의 역전 IoC

오브젝트가 생성되고 여타 오브젝트와 관계를 맺는 작업의 제어권을 별도의 오브젝트 팩토리를 만들어서 넘겼다.

또한 오브젝트 팩토리의 기능을 일반화한 IoC 컨테이너로 넘겨서 오브젝트가 자신이 사용할 대상의 생성이나 선택으로부터 자유롭게 만들었다.

싱글톤 레지스트리

전통적인 싱글톤 패턴의 특징 및 장단점을 보고, 서버에서 싱글톤 패턴이 사용되기 위한 컨테이너를 활용하는 방법을 알아봤다.

의존관계 주입 DI

설계 시점과 코드에는 클래스와 인터페이스 사이의 느슨한 의존관계만 만들어놓고, 런타임 시에 실제 사용할 구체적인 의존 객체를 DI 컨테이너를 통해서 주입받게 했다.

스프링이란 어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크

스프링의 관심은 오브젝트와 관계이다.

  • 오브젝트를 어떻게 설계, 분리, 개선, 의존관계 설정 등을 할지는 개발자의 몫이다.
반응형

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

3장 - 템플릿  (0) 2021.08.20
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday