Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5주차] (오형석, 김수환, 권용현) #1

Open
sunwootest opened this issue Jul 25, 2023 · 3 comments
Open

[5주차] (오형석, 김수환, 권용현) #1

sunwootest opened this issue Jul 25, 2023 · 3 comments
Assignees
Labels

Comments

@sunwootest
Copy link
Collaborator

  • 5주차
    • 1장
    • 2장
    • 3장
@genius00hwan
Copy link

genius00hwan commented Jul 31, 2023

[1장 오브젝트와 의존관계]

객체지향설계
객체는 어떻게 설계돼야 하나, 어떤 단위로 만들어지고 어떤 과정으로 객체의 존재를 들어내는가

DAO의 분리

객체지향 설계는 미래의 변화에 대비할 수 있어야 한다.

관심사의 분리

  • 미래의 변화에 대비
    -> 분리와 확장을 고려한 설계
  • 대체로 하나의 관심에 대해 변화가 일어남
    -> 관심이 같은 것 끼리는 하나의 객체, 근처 객체로 모아

중복 코드의 메소드 추출

메소드 여러개의 중복되는 코드가 있다면 이를 하나의 메소드로 만들어주자.

ex) userDAO의 db랑 연결하는 코드가 메소드마다 있음
db종류 바뀐다거나, url이 바뀐다거나 등의 이슈 -> 모든 메소드의 코드를 다 바꿔야 함
db연결 메소드 getConnection()이란 메소드를 만들어서 다른 메소드에서 이를 호출하도록 변경
public void add(User user) throws ClassNotFoundException, SQLException {
    Connection c = getConnection(); 
    ...
}
public User get(String id) throws ClassNotFoundException, SQLException { 
    Connection c = getConnection();
    ...
}

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

리팩토링

: 변경사항에 대한 검증

  • 리팩토링 :
    기능은 유지 하면서 코드의 구조를 더 깔끔하게 기존의 동작방식에 변화 없이 내부 구조만 변경해서 재구성
    유지보수 용이, 가독성 up

테스트를 통해 기능이 유지되는지 검증할 수 있다.

상속을 통한 확장

  • 코드를 여러 프로젝트에서 재사용 하려고 한다.
  • 프로젝트 마다 사용하는 db가 다르다면?
  • 상속을 이용 -> 추상클래스 사용
    핵심 기능은 그대로 사용
    db연결 메소드만 프로젝트에서 사용하는 db에 맞춰 사용
public abstract class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException {
    Connection c = getConnection(); 
}
public User get(String id) throws ClassNotFoundException, SQLException { 
    Connection c = getConnection();
} 
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
public class AUserDao extends UserDao {
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // A 프로젝트 DB connection 생성코드 
        }
}
public class BUserDao extends UserDao {
    public Connection getConnectionO throws ClassNotFoundException,
        SQLException {
            // B프로젝트 DB connection 생성코드
        } 

}

템플릿 메소드 패턴

상속을 통해 슈퍼클래스의 기능을 확장하기 위한 디자인 패턴

  • 변하지 않는 기능은 슈퍼클래스에 만들고 자주 변경되며 확장할 기능은 서브클래스에서 구현
  • 슈퍼클래스에 기본적인 로직의 흐름을 나타낸다.
  • 서브클래스에서 메소드를 필요에 맞게 구현

팩토리 메소드 패턴

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

  • 슈퍼클래스에서 미리 구현할 메소드를 호출해서 사용
  • 서브클래스에서 생성방법, 클래스를 재정

상속의 문제점

  • 자바는 다중상속을 허용하지 않음
  • 슈퍼클래스 변경 -> 서브 클래스도 변경
  • 확장된 기능을 다른 클래스에 적용할 수 없음 -> 또 코드 중복

dao의 확장


오브젝트는 변한다. 변화에는 특징이 있다. 변화 마다 각기 대응 방식이 다르다

클래스의 분리

  • 변화의 성격이 다른 코드를 다른 클래스로 분리하자
  • ex)
    userDao에 있던 db연결 코드를 다른 클래스로 분리
    db연결 객체를 생성해서 userDAO에서 사용
    코드를 여러 프로젝트에서 재사용 할 수 있음
public class UserDaoTest {
    public static void main(String[] args) 
    	throws ClassNotFoundException, SQLException {
    ConnectionMaker connectionMaker = new AConnectionMaker(); 
    // A 프로젝트에서 사용하는 db연동 
    UserDao dao = new UserDao(connectionMaker); 
    // 구현 클래스 결정 -> 객체 생성
    }
}

프로젝트 마다 사용하는 db가 다름 -> 같은 문제가 발생

인터페이스의 도입

  • 인터페이스를 이용해 추상화
  • 인터페이스를 이용해 객체를 생성하여 사용
  • 객체를 생성하는 코드에서 인터페이스 구현에 따라 각기 다른 객체를 생성해야함
  • ex)
    blogRepository에 기본적인 로직의 흐름을 작성하고
    memoryBlogRepository에 구체적인 구현을 함
    db사용 시 dbBlogRepository만 작성해주면 됨

관계설정 책임의 분리

클래스간의 관계를 설정하는 방법

  • 의존객체에서 해당 객체를 생성 : 구체적인 클래스를 알아야 사용
  • 의존 주입 : 인터페이스로 파라미터를 넘기고 실제 사용할때 구체적으로 클래스를 정해줌
    • 유연한 방법으로 클래스 분리
    • 필요따라 자유롭게 확장

원칙과 패턴

객체지향 설계를 위한 여러가지 방법

개방 폐쇄 원칙

기존코드를 수정하지 않고 기능을 추가할 수 있어야 한다.
  • 확장에는 개방적, 수정에는 폐쇄적
  • 인터페이스로 제공되는 확장에는 개방
  • 인터페이스를 이용하는 클래스의 변화에는 폐쇄

높은 응집도와 낮은 결합도

  • 높은 응집도 : 하나의 모듈, 클래스는 하나의 책임, 관심사에 집중되어있다.
    응집도가 낮으면 변경하려는 부분이 어딘지 찾아야됨
  • 낮은 결합도 : 객체간 연결이 최소한의 방법으로 간접적인 형태로 제공된다.
    높은 결합도 -> 클래스간 관계가 긴밀하여 작은 변경에도 다른 모듈과 객체에 영향이 큼

전략패턴

  • 자신의 기능 context 에서 필요에 따라 변경이 필요한 알고리즘은
    인터페이스를 이용해 외부로 분리한다.
  • 이를 구현한 알고리즘을 필요에 따라 바꿔서 사용할 수 있게 할 수 있는 디자인패턴
  • ex)
blogRepository를 이용해 db연결에 따라 변경하도록 함
실제 구현한 알고리즘은 memoryBlogRepository로 분리
dbBlogReopsitory구현하여 필요에 따라 바꿔서 사용 가능


제어의 역전 (IoC : Inversion of Control)

낮은 결합도를 활용하여 테스트, 유지관리를 더쉽게 하기 위한 설계 원칙

오브젝트 팩토리

팩토리 : 객체 생성 방법을 결정 -> 객체를 반환

  • 객체를 생성하는 쪽, 생성된 객체를 사용하는 쪽으로 역할을 나누려는 목적으로 사용
  • ex)
    DaoFactory라는 클래스 생성 -> DaoFactory에서 userDao, DB 연결 객체 생성
    db 연결 방식이 달라져도 DaoFactory로 자유로운 확장이 가능
public class DaoFactory { 
    public UserDao userDao() {
      
        ConnectionMaker connectionMaker = new AConnectionMakerO; UserDao userDao = new UserDao(connectionMaker);
        return userDao; 
    } 
}
  • 만약 다른 DAO의 생성기능을 넣어야 한다면?
public class DaoFactory { 
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }
    public AccountDao accountDaoO {
        return new AccountDao(connectionMaker());
    }
    public MessageDao messageDaoO {
        return new MessageDao(connectionMaker());
    }
    // ConnectionMaker를 생성하는 코드
    // 중복을 피하기 위해 따로 메소드를 구현
    public ConnectionMaker connectionMaker(){
    	return new AConnectionMaker();
    } 
   
}

제어의 역전이란?

프로그램의 제어 흐름 구조가 뒤바뀌는 것

  • 일반적으로 main(), application() 등과 같이 프로그램이 시작되는 지점에서 사용할 객체를 결정, 생성, 호출
  • 제어의 역전에서는 자신이 사용할 오브젝트를 스스로 선택 하지 않고, 제어 권한을 다른 대상에 위임한다.
  • 프레임워크도 제어의 역전 개념이 적용된 대표적인 기술
  • 프레임워크 위에 개발한 클래스를 등록 -> 프레임워크가 흐름을 주도하며 개발자가 만든 코드를 사용

스프링의 IoC

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

  • 빈(Bean) : 스프링이 제어권을 가지고 IoC 방식으로 직접 만들어 관계를 부여하는 객체

  • 빈 팩토리 : 스프링에서 빈의 생성과 관계설정과 같은 제어를 담당하는 IoC 객체

  • 애플리케이션 컨텍스트 : IoC 방식에 따라 만들어진 빈 팩토리,

  • 스프링 컨테이너 : 빈 팩토리이자 애플리케이션 컨텍스트
    의존관계 주입을 이용하여 애플리케이션을 구성하는 여러 빈들의 생명주기와 서비스 실행 등을 관리하며 생성된 인스턴스들에게 기능을 제공하는 것을 부르는 말이다.

  • 설정정보/ 설정 메타정보 : 스프링 컨테이너를 적용하기 위해 사용하는 메타정보.

  • 빈 팩토리라고 할 때는 빈을 생성하고 관계를 설정하는 IoC의 기본 기능 에 초점을 맞춘 것.

  • 애플리케이션 컨텍스트는 별도의 정보를 참고해서 빈의 생성, 관계 설정 등의 제어를 총괄하는 것 에 초점을 맞춘 것

스프링 컨테이너의 동작방식

  • 애플리케이션에서 IoC를 적용해서 관리할 모든 객체에 대한 생성과 관계설정을 담당한다.
  • 생성, 관계설정에 대한 코드가 없고 설정정보를 통해 생성정보와 관계설정에 대한 정보를 얻는다.
  • @configuration 어노테이션을 통해 설정정보를 나타낼수 있다.
  • @bean 어노테이션을 통해 객체 생성정보를 표현할 수 있다.

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

싱글톤 패턴

객체의 인스턴스를 한개만 생성되게 하는 패턴

  • 프로그램 내에서 하나의 객체만 존재해야 한다.
  • 프로그램 내에서 여러 부분에서 해당 객체를 공유하여 사용해야한다.

서버 어플리케이션과 싱글톤

  • 클라이언트의 요청이 들어올 때 마다 객체를 새로생성
    -> 과부하, 서버가 감당할 수 없음
  • 서블릿 클래스당 하나의 객체만 만들어서 클라이언트의 요청에 따라 객체를 공유해서 사용

싱글톤의 한계

  • 상속 불가 : private 생성자
  • 테스트에 어려움
    생성 방식이 제한적이기 때문에 mock 객체로 대체하기 어려움
  • 서버환경
    클래스 로더 구성에 따라 (jvm의 분산 설치) 하나 이상의 객체가 생성 될 수 있음

싱글톤 레지스트리

스프링은 싱글톤 패턴으로 객체를 생성하고 관리한다.

  • static, private 의 사용과 상관없이 싱글톤 패턴을 사용할 수 있다.
  • 테스트 환경에서 자유롭게 객체를 생성할 수 있다.
  • 객체 지향적 설계와 디자인 패턴을 적용하는 데에 무리가 없다.

싱글톤과 오브젝트의 상태

멀티 스레드 환경에서 여러 스레드가 동시에 접근 할 수 있다.

  • 상태가 없어야한다.
  • 로컬 변수와 파라미터를 이용한다.
    독립적인 공간이 할당되기 때문에 여러 스레드가 값을 덮어쓰지 않는다.

스프링 빈의 스코프

객체가 생성되고 존재하고 적용되는 범위

싱글톤 스코프

  • 스프링 빈의 기본 스코프는 싱글톤이다.
  • 컨테이너에 하나의 객체만 만들어지고 강제로 제거하지 않는 한 스프링 컨테이너가 유지되는 동안 존재한다.

그 외의 스코프

  • 프로토타입 스코프
  • 요청 스코프
  • 세션 스코프

의존관계 주입

의존관계란?

A ----> B : A가 B에 의존하고 있다.

B의 변화가 A에 영향을 미친다.

ex)

blogService의 update() 파라미터가 바뀌면?
blogController에서 blogService.update()를 호출할 때 사용하는 파라미터가 바뀌어야 한다.

제어의 역전(IoC)과 의존관계 주입

IoC를 적용하여 객체지향적인 설계, 디자인 패턴, 컨테이너에서 동작하는 서버 기술을 사용한다.

의존관계 검색과 주입

  • 의존관계 검색
    의존관계를 맺을 객체를 결정, 생성 하는 작업은 외부 컨테이너에게 IoC에 위임
    객체를 가져올 때는 스스로 컨테이너에게 요청하는 방법을 사용
    빈 객체가 아닌경우 test 메소드등 객체를 주입받을 방법이 없는 경우에 사용
  • 의존관계 주입
    의존 관계의 객체를 가져올 때 메소드, 생성자를 통해 주입
    빈 객체 사이에서만 가능

메소드를 이요한 의존관계 주입

  • Setter를 이용한 주입
    수정자(Setter) 메소드는 외부에서 객체 내부의 속성값을 변경하려는 용도로 사용
  • 일반 메소드를 이용한 주입
    수정자 메소드처럼 set으로 시작. 여러 개의 파라미터를 받을 수 있음

정리

  • 스프링 : '어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크'
    원칙을 잘 따르는 설계를 적용하려고 할 때 귀찮은 작업을 편하게 할 수 있도록 도와주는 유용한 도구

  • 개발자 : 객체를 어떻게 설계하고, 분리하고, 개선하고, 어떤 의존관계를 가질지 결정.

스프링을 사용한다고 좋은 객체지향 설계와 깔끔하고 유연한 코드가 저절로 만들어지는건 절대 아님

@kwonyonghyun
Copy link

kwonyonghyun commented Jul 31, 2023

[토비의 스프링 2장] 테스트

테스트란 무엇이며, 그 가치와 장점, 활용 전략, 스프링과의 관계를 살펴본다.


테스트 코드란?
소프트웨어의 기능과 동작을 테스트하는데 사용되는 코드

테스트 코드를 작성해야 하는 이유는 뭘까?
작성한 코드를 확신할 수 있게 해 주고, 변화에 유연하게 대처할 수 있는 자신감을 준다.
(ex 리팩토링을 할 경우에 찝찝한 느낌을 최소화 할 수 있다)

그런테 테스트 코드가 왜 필요해? 그냥 로컬에서 띄워서 돌아가는지 확인하면 되는거 아니야?

웹에서 테스트를 하기 위해서는 모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하고, 실패의 원인이 테스트 코드가 아닐 가능성도 존재한다.
->그래서 나온 것이 단위 테스트

단위 테스트란?
외부에 의존하지 않는 하나의 관심사에 집중하여 테스트하는 것

단위 테스트를 이용함으로써 어느 부분에서 문제가 생기는지 상대적으로 명확하게 판단 가능

**그래도 단위 테스트를 사용 하더라도 사람이 확인을 해야되는 거잖아...? **->JUnit 프레임워크가 해결해준다

JUnit이란?
테스트 검증을 자동화 시켜주는 프레임워크


좋은 테스트는 어떤 테스트일까?

  • 단위가 작은 테스트
  • 확인의 대상과 조건이 간단하고 명확한 테스트
  • 외부 영향에 상관없이 동일한 결과를 내는 테스트
  • 네커티브 테스트가 있는 테스트

개발하는 방법중에는 TDD(테스트 주도 개발)이라는 것도 있다!

  • 테스트 코드를 먼저 만들고, 그에 맞는 구현 코드를 작성하는 순서로 이루어진다
  • 실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다

만들어진 모든 코드는 빠짐없이 테스트로 검증된다!

TDD 기능 설계 테스트 전략

추가하고 싶은 기능을 구현 전에 먼저 테스트 코드로 구현한다.
조건, 행위, 결과 3개로 표현하는 것이 좋다.

  • Given, When, Then (Given-When_Then 패턴)
  • Arrange, Act, Assert (AAA 패턴)

Junit 프레임워크의 동작 방식

  1. public void 메서드 시그니처를 가지고 있는 테스트 메서드를 모두 조회한다.
  2. 테스트 클래스에서 사용될 오브젝트를 하나 만든다.(이 오브젝트는 매 메소드마다 한번씩 생성되고 버려짐->완전한 독립적 실행 보장)
  3. @beforeeach 가 붙은 메서드가 있는지 찾고 각 테스트 전에 먼저 실행하게 된다.
  4. @test가 붙은 메서드를 호출하고 테스트 결과를 저장해둔다.
  5. @AfterEach 가 붙은 메서드가 있는지 찾고 각 테스트 이후에 실행하게 된다
  6. 위의 2,3,4,5번을 모든 테스트 메서드에 대해 반복
  7. 모든 테스트의 결과를 종합해서 반환한다.

픽스처란?

테스트에서 사용될 정보나 오브젝트( 보통은 @beforeeach와 같은 메서드를 이용해서 제공한다)
시간이나 자원들을 많이 차지하는 픽스처들은 하나의 픽스처 공유(@Autowired 이용)

_⭐️스프링 컨테이너를 띄우지 않아도 되는 테스트라면 띄우지 않는 것이 가장 좋다._⭐️

public class UserDaoTest {
    UserDao userDao;

    @BeforeEach
    public void setUp() {
        ApplicationContext applicationContext = new GenericXmlApplicationContext("spring/applicationContext.xml");
        this.userDao = applicationContext.getBean(UserDao.class);
        ...
    }
    ...
}
@ExtendWith(SpringExtension.class) // (JUnit5)
@ContextConfiguration(locations="/spring/applicationContext.xml")
public class UserDaoTest {
    @Autowired ApplicationContext applicationContext;
    UserDao userDao;

    @BeforeEach
    public void setUp() {
        System.out.println("applicationContext = " + applicationContext);
        System.out.println("this = " + this);
        this.userDao = this.applicationContext.getBean("userDao", UserDao.class);
        ...
    }
    ...
}

인터페이스를 이용하면 테스트를 짜기가 더 쉬워진다?

  • 인터페이스를 DI에 이용하면 구현체의 복잡한 테스트를 할 필요 없이 인터페이스에 공개된 메서드를 작게 테스트하면 된다.

그럼 테스트 할때 데이터베이스는 어떻게 해?

  • 데이터 소스를 다른 것으로 변경하여 실행(->@DirtiesContext 반드시 명시)

여기서 궁금했던것: 어노테이션 없어도 어차피 배포용 데이터베이스를 건들지 않는데 어노테이션의 쓸모?

  • 이 어노테이션을 통해 테스트를 수행하기 전, 수행한 이후, 그리고 테스트의 각 테스트 케이스마다 수행하기 전, 수행한 이후에 context를 다시 생성하도록 지시할 수 있다.

(extra: 테스트 코드를 돌리다 보면 클래스 단위로 돌릴때 실패하던 테스트 코드가 메소드 단위로 돌리면 성공하는 경우가 있는데 이때 문제가 @DirtiesContext 안적어줘서 그럴 수도 있음)

학습 테스트란?
애플리케이션의 로직보다는 API의 기능 확인 등을 위해 작성해보는 테스트

  • 작동 방식을 확인할 수 있다

버그 테스트란?
코드에 오류가 있을 때, 해당 오류를 가장 잘 나타내는 테스트를 만드는 것. 코드를 수정하여 버그 테스트가 성공하면 버그 수정이 성공한 것.

  • 테스트의 완성도를 높인다
  • 테스트에서 버그의 내용을 명확히 나타내준다.
  • 기술적인 문제를 해결하는데 도움이 된다.

버그 테스트 vs 네거티브 테스트

  • 버그 테스트는 소프트웨어의 정상적인 동작을 확인하고 문제점을 찾는 것
  • 네거티브 테스트는 예외 상황과 잘못된 입력 등으로 소프트웨어가 어떻게 반응하는지를 확인하는 것

@kuk6933
Copy link

kuk6933 commented Jul 31, 2023

템플릿 ?
바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있는 방법

변하는 것과 변하지 않는 것

public void deleteAll() throws SQLException {
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("DELETE FROM users")
    ps.executeUpdate();

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

→ error가 발생하면 close 메소드가 실행되지 않아 리소스가 반환되지 않을 수 있음

public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
        ps = c.prepareStatement("DELETE FROM users");
        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if (ps != null)
            try {
                ps.close();
            } catch (SQLException e) {
                // 이제 여기서 ps 리소스 반환 실패를 잡을 수 있다
            }
        if (c != null) 
            try {
                c.close();
            } catch (SQLException e) {
            }
    }
}

→ close는 해줄 수 있겠으나 모든 메서드에 try/catch/finally를 다 붙이면 가독성도 안좋고 중복도 많이 발생

어떻게 해결할 수 있을까?

  1. 변하는 부분을 메소드로 추출
private PreparedStatement makeStatement(Connection c) throws SQLException{
    PreparedStatement ps;
    ps = c.prepareStatement("delete from user");
    return ps;
}

→ 변하는 부분은 매번 새로 만들어줘야함

  1. 템플릿 메소드 패턴 적용

템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분. 변하지 않는 부분은 슈퍼 클래스에 두고 변하는 부분은 추상 메서드로 정의해서 서브 클래스에서 오버라이드하여 새롭게 정의해 쓰는 것.

public abstract class UserDao {
    // executeDao()같은 메소드를 두고, 매번 반복되는 try~finally 구문을 기록하고
    // 그 사이에 이 아래 makeStatement()를 끼워 넣어둔다
    
    public abstract PreparedStatement makeStatement(Connection c) throws SQLException;
    
}
public class UserDaoDeleteAll extends UserDao{
    private PreparedStatement makeStatement(Connection c) throws SQLException{
        PreparedStatement ps;
        ps = c.prepareStatement("delete from user");
        return ps;
    }

makeStatement()만 바뀌고 새로 메소드를 만들때도 이 메서드만 보면 됨.

하지만 두가지 문제점이 존재

  • 모든 DAO 메서드 마다 상속을 통해 새로운 클래스를 만들어야함
  • 컴파일 시점에 관계가 결정되어있어 유연하지 못함
  1. 전략 패턴 적용

OCP 를 잘 지키는 구조이면서도 템플릿 메서드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
public class DeleteAllStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = null;
        ps = c.prepareStatement("DELETE FROM users");
        return ps;
    }
public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
        StatementStrategy stmt = new DeleteAllStatement(); //여기만 바꾸면 됨
        ps = stmt.makePreparedStatement(c);
        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if (ps != null)
            try {
                ps.close();
            } catch (SQLException e) {
            }
        if (c != null) try {
            c.close();
        } catch (SQLException e) {

        }
    }
}

→ 전략 패턴을 그럭저럭 적용함

하지만 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔쓸 수 있음(OCP)

그러나 위 코드는 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있음.

컨텍스트가 StatementStrategy 인터페이스 뿐 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는건 전략 패턴에도 OCP에도 잘 들어맞는다고 할 수 없음

위 문제를 어떻게 해결?

Context를 사용하는 Client가 어떤 전략을 사용하게 할 것인지 정하도록 하자

// Client
public void deleteAll() throws SQLException {
    StatementStrategy stmt = new DeleteAllStatement();
    jdbcContextWithStatementStrategy(stmt);
}

// Context
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException{
    // SQL 쿼리(PreparedStatement)와는 결합도가 낮은 "JDBC 작업 흐름"을 분리해 냄
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        ps = stmt.makeStatement(c); // 이 부분에서 strategy 사용

        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if (ps != null)
            try {
                ps.close();
            } catch (SQLException e) {
            }
        if (c != null) try {
            c.close();
        } catch (SQLException e) {
        }
    }
}

이제 구조로 볼 때 완벽한 전략 패턴의 모습을 갖추게 됨. 클라이언트와 컨텍스트는 클래스를 분리하지 않았지만 의존관계와 책임으로 볼 때 이상적인 클라이언트/컨텍스트 관계를 갖게됨. 클라이언트가 컨텍스트가 사용할 전략을 전달한다는 면에서 DI구조로 봐도 됨.

JDBC 전략 패턴의 최적화

아직 개선할 점이 남아있음.

  1. DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야함
  2. DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적보가 있는 경우 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 만들어둬야함.

→ 어차피 각 전략은 각 메소드에서만 쓰이니까 local class를 사용하자

public void add(final User user) throws SQLException {
  class AddStatement implements StatementStrategy {   //  add() 메소드 내부에 선언된 로컬 클래
      User user;

      public AddStatement(User user) {
          this.user = user;
      }

      public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
          PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
          ...
      }

      StatementStrategy st = new AddStatement(user);
      jdbcContextWithStatementStrategy(st);
  }
}

→ 메소드마다 추가해야했던 클래스 파일을 줄일 수 있음

→ 로컬 변수를 바로 가져다 사용할 수 있음

익명 클래스를 쓰면?

public void add(final User user) throws SQLException {
    jdbcContextWithStatementStrategy(
        new StatementStrategy() {
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
    
                ps.setString(1, user.getId());
                ps.setString(2, user.getName();
                ...
                return ps;
            }
        }
    );
}

훨씬 간결해짐

jdbcContextWithStatementStrategy()는 UserDao외에도 적용할 수 있으므로 별도 클래스로 독립시켜 모든 DAO가 사용할 수 있도록 해보자

JdbcContext라는 컨텍스트를 만들었늗네 이러면 DataSource가 필요한 것은 JdbcContext가 되어버림. 그러므로 JdbcContext가 DataSource를 DI받을 수 있도록 해야함

public class JdbcContext {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {  //  DataSource 타입 빈을 DI 받을 수 있게 준비
        this.dataSource = dataSource;
    }

    public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {...} 
        catch (SQLException e) {...}
        finally {...}
    }
}
public class UserDao {
    ...
    private JdbcContext jdbcContext;

    public void setJdbcContext(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext;             //  jdbcContext를 Di받도록 만든다.
    }

    public void add(final User user) throws SQLException {
        this.jdbcContext.workWithStatementStrategy(     //  DI 받은 JdbcContext의 컨텍스트 메소드를 사용하도록 변경한다.
            new StatementStrategy() {...}
        );

그런데 UserDao는 구체 클래스에 의존하고 있음.

하지만 이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 '서비스 오브젝트'로서 의미가 있을 뿐이고 구현 방법이 바뀔 가능성은 거의 없다"라고 함.

→ Jdbc가 아닌 JPA나 하이버네이트같이 ORM을 사용해야 하면 아예 싹다 갈아야함.

JdbcContext를 UserDao와 DI구조를 만들어야할 이유?

  • JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되는게 이상적이기 때문(변경되는 상태정보를 갖고있지 않기 때문)
  • JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문

수동 DI?

public class UserDao {
    JdbcContext jdbcContext;
    DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
        this.jdbcContext = new JdbcContext();
        this.jdbcContext.setDataSource(dataSource);
    }
    
    // ...
    
}

→ 긴밀한 관계를 가진 두 객체를 어색하게 빈으로 분리 안해도 됨.

→ UserDao 내부에서 DI를 적용하는 방식

but 싱글톤X, DI를 위한 추가 코드도 발생함

템플릿/ 콜백 패턴

템플릿은 고정된 작업 흐름을 가진 코드를 재사용 한다는 의미에서 붙은 이름

콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트

전략패턴의 컨텍스트를 템플릿이라고 부르고

익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부름(콜백은 보통 단일 메소드 인터페이스)

스크린샷 2023-08-01 오전 2 35 57
  • 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공하는 것이다. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다.
  • 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다. 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다,
  • 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다. 경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다

DI 방식의 전략 패턴 구조라고 생각하면 됨.

콜백의 분리와 재활용

public void deleteAll() throws SQLException {
    executeSql("delete from users");
}

private void executeSql(final String query) throws SQLException {
    this.jdbcContext.workWithStatementStrategy(
        new StatementStrategy() {   //  변하지 않는 콜백 클래스 정의와 오브젝트 생성
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                return c.preparedStatement(query);
            }
        }
    );
}

이렇게 바뀌는 부분과 바뀌지 않는 부분을 분리 가능

콜백과 템플릿의 결합

public class JdbcContext {
    ...
    public void executeSql(final String query) throws SQLException {
        workWithStatementStrategy(
            new StatementStrategy() {...}
        );
    }
}

이렇게 JdbcContext 클래스로 콜백 생성과 템플릿 호출이 담긴 executeSql을 옮기면 모든 DAO 메소드에서 사용 가능

템플릿/콜백은 스프링이 객체지향 설계와 프로그래밍에 얼마나 가치를 두고 있는지를 잘 보여주는 예시임. 스프링이 제공하는 템플릿/콜백을 잘 사용해아 하는것은 물론이고 직접 템플릿/콜백을 만들어 활용할 수도 있어야함

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants