본문 바로가기
320x100
320x100

1장 오브젝트와 의존관계


1.1 초난감 DAO

1.2 DAO의 분리

1.3 DAO의 확장

1.4 제어의 역전(IoC)

1.5 스프링의 IoC

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

1.7 의존관계 주입(DI)

1.8 XML을 이용한 설정

1.9 정리


직접 코드를 치면서 따라오고싶은 분들은 https://code-boki.tistory.com/20

 

IntelliJ에서 Spring 프로젝트(Spring MVC) 생성하기

요즘은 Spring Boot로 바로 개발한다. 하지만 Spring을 제대로 알려면 Servlet부터 Spring, Spring Boot 순서로 배워야 잘 배운거라 생각한다! 옛날 포스팅된 글들 혹은 이클립스, STS로 스프링프로젝트를 만

code-boki.tistory.com

를 참고해서 인텔리제이로 스프링 프로젝트를 만들어보길 바란다.

먼저 공부하기에 앞서 스프링이란 무엇인가?에 대해서 알아보고자 한다.

- 스프링이란 무엇인가?

스프링은 자바 엔터프라이즈 애플리케이션 개발에 사용되는 애플리케이션 프레임워크다.

애플리케이션 프레임워크는 애플리케이션 개발을 빠르고 효율적으로 할 수 있도록 애플리케이션의 바탕이 되는 틀과 공통 프로그래밍 모델, 기술 API 등을 제공해준다.

네이버에서 엔터프라이즈를 검색한 결과 기업, 회사라는 뜻이 나온다. 대기업과 의미가 일맥상통한다고 봐도 틀린 말이 아니다.

회사에서 쓰일 수 있게 개발된 작업틀(프레임워크)인 것이다.

  • 애플리케이션의 기본 틀 - 스프링 컨테이너
  • 공통 프로그래밍 모델 - IoC/DI, 서비스 추상화, AOP
  • 기술 API

     스프링은 스프링 컨테이너 또는 애플리케이션 컨텍스트라고 불리는 스프링 런타임 엔진을 제공한다.(ApplicationContext - AC)

스프링 컨테이너는 설정정보를 참고로 해서 애플리케이션을 구성하는 오브젝트를 생성하고 관리한다.

스프링 컨테이너는 독립적으로 동작할 수도 있지만 보통 웹 모듈에서 동작하는 서비스나 서블릿으로 등록해서 사용한다.

 

    프레임워크는 애플리케이션을 구성하는 오브젝트가 생성되고 동작하는 방식에 대한 틀을 제공해줄 뿐만 아니라, 애플리케이션 코드가 어떻게 작성돼야 하는지에 대한 기준도 제시해준다. 이런 틀을 보통 프로그래밍 모델이라고 한다. 스프링은 세 가지 핵심 프로그래밍 모델을 지원한다.

첫 번째는 IoC/DI라고 불리는 오브젝트의 생명주기와 의존관계에 대한 프로그래밍 모델이다. 스프링은 유연하고 확장성이 뛰어난 코드를 만들 수 있게 도와주는 객체지향 설계 원칙과 디자인 패턴의 핵심 원리를 담고 있는 IoC/DI를 프레임워크의 근간으로 삼고 있다.

두 번째는 서비스 추상화다. 구체적인 기술과 환경에 종속되지 않도록 유연한 추상 계층을 두는 방법이다.

세 번째는 AOP다. AOP는 애플리케이션 코드에 산재해서 나타나는 부가적인 기능을 독립적으로 모듈화하는 프로그래밍 모델이다. 어떻게 보면 관점에 따라 코드를 자동 삽입해준다고 이해해도 틀린 말은 아니다.

 

    스프링은 엔터프라이즈 애플리케이션을 개발의 다양한 영역에 바로 활용할 수 있는 방대한 양의 기술 API를 제공한다. 스프링이 제공하는 API와 지원 기술은 모두 스프링의 프로그래밍 모델에 따라 작성되어 있기 때문에, 이를 가져다 쓰는 것만으로도 스프링의 프로그래밍 모델을 코드에 자연스럽게 적용할 수 있다. 스프링의 모든 기술은 표준 자바 엔터프라이즈 플랫폼(Java EE)에 기반을 두고 있다. 표준 기술과 더불어 유명 오픈소스 기술과 주요 상용 기술에 대한 지원 기능도 다양하게 제공된다.

 

- 스프링의 성공요인

1. 단순함(simplicity)

스프링은 EJB라는 강한 권위를 가졌던 표준 기술을 비판하면서 등장했다. EJB(Enterprise Java Beans)는 불필요하게 복잡한 기술이었기 때문이다. 스프링이 지향하는 것은 목적을 이룰 수 있는 가장 단순하고 명쾌한 접근 방법이다. 스프링이 강력히 주장하는 것은 가장 단순한 객체지향적인 개발 모델인 POJO(Plain Old Java Object) 프로그래밍이다. 스프링은 POJO 프레임워크이다.

2. 유연성(flexibility)

스프링은 유연성을 중요한 가치로 내세운다. 스프링은 유연성과 확장성이 매우 뛰어나다. 스프링의 유연성으로 인해 스프링은 다른 많은 프레임워크와 편리하게 접목돼서 사용될 수 있다. 스프링은 프레임워크를 위한 프레임워크 또는 여러 프레임워크를 함께 사용하게 해주는 접착(glue) 프레임워크라고도 불린다.

스프링은 스스로 발전하는 프레임워크다. 스프링 개발 철학 중 하나는 "항상 프레임워크 기반의 접근 방법을 사용하라"이다. 스프링은 개발자들에게 스프링을 확장해서 사용하도록 권장한다. 프레임워크를 확장해서 사용한 뒤 시간이 지남에 따라 경험하게 되는 버전 호환성 문제가 스프링에는 거의 없다.


1.1 초난감 DAO

* DAO(Data Access Object): DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트

* JavaBean(*Bean): 원래는 비주얼 툴에서 조작 가능한 컴포넌트를 말했다. 이제는 자바빈이라고 말하면 디폴트 생성자를 가지고, 프로퍼티(get&set)을 가진 두 가지 관례를 따라 만들어진 오브젝트를 가리킨다. 간단히 빈이라고 부르기도 한다.

 

간단한 DAO를 하나 만들어본다.

public class User {
    String id;
    String name;
    String password;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

User 오브젝트에 담긴 정보가 실제로 보관될 DB 테이블을 하나 만들어보자. 테이블 이름은 users로 만든다.

나같은 경우 MySQL을 이용했다. DB에 종속된 예제가 아니기때문에 다른 DB도 상관없다.

CREATE TABEL users (
	id varchar(10) primary key,
    name varchar(20) not null,
    password varchar(10) not null
);

사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만들어보자.

mysql, mysql-connector-java 둘다 버전 8을 이용했다.

# pom.xml에 의존성 추가

<dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.22</version>
</dependency>
public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection(
            "jdbc:mysql://localhost/study_toby?characterEncoding=UTF-8&serverTimezone=UTC",
            "toby", "7882"
        );
        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("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection(
            "jdbc:mysql://localhost/study_toby?characterEncoding=UTF-8&serverTimezone=UTC",
            "toby", "7882"
        );
        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;
}

main() 함수를 이용해서 DAO 테스트 코드를 작성해본다(사실 올바른 방법이 아니다. 뒤에 나온다)

    public static void main(String[] args) throws ClassNotFoundException, SQLException{
        UserDao dao = new UserDao();

        User user = new User();
        user.setId("whiteship");
        user.setName("백기선");
        user.setPassword("married");

        dao.add(user);

        System.out.println(user.getId() + " 등록 성공");

        User user2 = dao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getPassword());
        System.out.println(user2.getId() + " 조회 성공");
    }

잘 작동한다.

그런데 지금 만든 UserDao 클래스 코드에는 사실 여러가지 문제가 있다.

실제 프로젝트에서 자바 개발자가 이렇게 DAO를 개발했다면 개발팀에서 바로 쫓겨나지 않을까 싶을 만큼 한심한 코드다.

여기서 내가 공부하는 철학인 Why?가 나온다. 왜? 잘 동작하는데 무슨 문제가 많다는거지..?라고 생각해보고 해결법을 떠올려보자


1.2 DAO의 분리

1. 관심사의 분리

    세상에는 변하는 것과 변하지 않는 것이 있다. 하지만 객체지향의 세계에서는 모든 것이 변한다. 여기서 변한다는 것은 변수나 오브젝트 필드의 값이 변한다는 게 아니다. 오브젝트에 대한 설계와 이를 구현한 코드가 변한다는 뜻이다. 소프트웨어 개발에서 끝이란 개념은 없다. 사용자의 비즈니스 프로세스와 그에 따른 요구사항은 끊임없이 바뀌고 발전한다. 애플리케이션이 기반을 두고 있는 기술도 시간이 지남에 따라 바뀌고, 운영되는 환경도 변화한다. 애플리케이션이 더 이상 사용되지 않아 폐기처분될 때가 되어 변화는 중지한다.

     그래서 개발자가 객체를 설계할 때 가장 염두해 둬야 할 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다. 미래를 준비하는 데 있어 가장 중요한 과제는 변화에 어떻게 대비할 것인가?이다. 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것이다.

     두 명의 개발자에게 동일한 기능 변경을 요청했다고 하자. 한 명은 단 몇 줄의 코드만 수정하고, 그 변경이 나머지 기능에는 전혀 문제를 일으키지 않는다는 것까지 검증해주는 데 5분이 걸렸다. 그런데 다른 개발자는 동일한 기능을 변경하는 데 5시간이 걸린데다, 그것이 기존에 잘 동작하던 다른 기능에 오류를 일으킬지도 모른다는 새로운 불안감까지 일으켰다면 과연 어떤 개발자가 더 미래의 변화를 잘 준비한 것일까? 정답은 알 것이라 생각한다.

    그러면 어떻게 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있었을까? 그것은 바로 분리와 확장을 고려한 설계가 있었기 때문이다. 

 

변화가 한 번에 한 가지 관심에 집중돼서 일어난다면, 우리가 준비해야 할 일은 한가지 관심이 한 군데에 집중되게 하는 것이다. 즉 관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것이다.(소프트웨어공학적으로는 응집도는 높이고, 결합도는 낮춤)

프로그래밍의 기초 개념 중에 관심사의 분리(Separation of Concerns)라는 것이 있다. 이를 객체 지향에 적용해보면, 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이라고 생각할 수 있다.

2. 커넥션 만들기의 추출

- UserDao의 관심사항

1. DB와 연결을 위한 커넥션을 어떻게 가져올까?

2. DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것

3. 작업이 끝나면 사용한 리소스인 Statement와 Connection 오브젝트를 다아줘서 소중한 공유 리소스를 시스템에 반환하는 것

 

- 중복 코드의 메소드 추출

가장 먼저 할 일은 커넥션을 가져오는 중복된 코드를 분리하는 것이다.

IntelliJ의 메소드 추출 단축키를 활용하자(option + command + M / ctrl + alt + m)

getConnection()이라는 메소드를 따로 만들었다.

    public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = 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("com.mysql.cj.jdbc.Driver");
        Connection c = 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;
    }

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(
            "jdbc:mysql://localhost/study_toby?characterEncoding=UTF-8&serverTimezone=UTC",
            "toby", "7882"
        );
    }

add와 get 메서드에서 중복해서 사용됐었던

Connection c = DriverManager.getConnection(
            "jdbc:mysql://localhost/study_toby?characterEncoding=UTF-8&serverTimezone=UTC",
            "toby", "7882"
        );

코드가 Connection c = getConnection(); 의 메서드로 중복을 제거했다.

 

- 변경 사항에 대한 검증: 리팩토링과 테스트

main()함수를 다시 실행시켜 잘 동작하는지 확인할 수 있다. 하지만 다시 실행하면 PRIMARY key의 duplicate로 인해서 예외가 일어난다.

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'whiteship' for key 'users.PRIMARY' 

DB안의 데이터를 다 지워주고 다시 실행하면 잘 동작하는 것을 확인할 수 있다

기능이 추가되거나 바뀐 것은 없지만 UserDao는 이전보다 훨씬 깔끔해졌고 미래의 변화에 좀 더 손쉽게 대응할 수 있는 코드가 됐다.

이런 작업을 리팩토링(refactoring)이라고 한다. 또한 위에서 사용한 getConnection()이라고 하는 공통의 기능을 담당하는 메소드로 중복된 코드를 뽑아내는 것을 리팩토링에서는 메소드 추출(extract method) 기법이라고 부른다.

* 리팩토링: 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술

 

3. DB 커넥션 만들기의 독립

아주 초보적인 관심사의 분리 작업이지만, 메소드 추출만으로 변화에 좀 더 유연하게 대처할 수 있는 코드를 만들었다. 이번엔 좀 더 나아가서 변화에 대응하는 수준이 아니라, 아예 변화를 반기는 DAO를 만들어보자.

앞에서 만든 UserDao가 N사와 D사에 사용자 관리를 위해 판매하는 상황이 되었다고 하자. 그런데 납품 과정에서 문제가 발생했다.

문제는 N사와 D사가 각기 다른 종류의 DB를 사용하고 있고, DB커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어한다는 점이다. 더욱 큰 문제는 UserDao를 구매한 이후에도 DB 커넥션을 가져오는 방법이 종종 변경될 가능성이 있다는 점이다.

이런 경우에는 아예 UserDao의 소스코드를 고객에게 제공해주고, 변경이 필요하면 getConnection() 메서드를 수정해서 사용하라고 할 수 있다. 하지만 초특급 비밀기술이 적용된 UserDao인지라 고객에게 소스를 직접 공개하고 싶지는 않다. 고객에게는 미리 컴파일된 클래스 바이너리 파일만 제공하고 싶다.

과연 이런 경우에 UserDao 소스코드를 N사와 D사에 제공하지 않고도 고객 스스로 원하는 DB커넥션 생성 방식을 적용해가면서 UserDao를 사용하게 할 수 있을까?

 

- 상속을 통한 확장

기존 UserDao 코드를 한 단계 더 분리하면된다. UserDao에서 메소드의 구현 코드를 제거하고 getConnection()을 추상 메소드로 만들어놓는다. 추상 메소드라서 메소드 코드는 없지만 메소드 자체는 존재한다.

따라서 add(), get() 메소드에서 getConnection()을 호출하는 코드는 그대로 유지할 수 있다.

public abstract class UserDao {

    public abstract Connection getConnection() throws SQLException;
    ....
}

public class NUserDao extends UserDao {

    @Override
    public Connection getConnection() throws SQLException {
        // N사 DB Connection 생성 코드
        return null;
    }
}

public class DUserDao extends UserDao {

    @Override
    public Connection getConnection() throws SQLException {
        // D사 DB Connection 생성 코드
        return null;
    }
}

intelliJ에서 클래스들을 선택하고 다이어그램을 만들면 이런 다이어그램을 그려준다

수정한 코드를 잘 살펴보자. DAO의 핵심 기능인 어떻게 데이터를 등록하고 가져올 것인가라는 관심을 담당하는 UserDao와, DB 연결 방법은 어떻게 할 것인가라는 관심을 담고 있는 NUserDao, DUserDao가 클래스 레벨로 구분이 되고 있다.

클래스 계층구조를 통해 두 개의 관심이 독립적으로 분리되면서 변경 작업은 한층 용이해졌다. 이제는 UserDao의 코드는 한 줄도 수정할 필요 없이 DB 연결 기능을 새롭게 정의한 클래스를 만들 수 있다.

 

이렇게 슈퍼클래스에 기본적인 로직의 흐름을 만들어 놓고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법디자인 패턴에서 템플릿 메소드 패턴(template method pattern)이라고 한다. 템플릿 메소드 패턴은 스프링에서 애용되는 디자인 패턴이다.

UserDao의 getConnection() 메소드는 Connection 타입 오브젝트를 생성한다는 기능을 정의해놓은 추상 메소드이다. 그리고 UserDao의 서브클래스의 getConnection() 메소드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지를 결정하는 방법이라고 볼 수 있다. 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것팩토리 메소드 패턴(factory method pattern)이라고 부르기도 한다.

* 디자인 패턴: 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션

* 템플릿 메소드 패턴: 상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. 슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드 할 수 있도록 만들어둔 메소드훅(hook) 메소드라고 한다. 서브클래스에서는 추상 메소드를 구현하거나, 훅 메소드를 오버라이드하는 방법을 이용해 기능의 일부를 확장한다.

* 팩토리 메소드 패턴: 팩토리 메소드 패턴도 템플릿 메소드 패턴과 마찬가지로 상속을 통해 기능을 확장하게 하는 패턴이다. 그래서 구조도 비슷하다. 슈퍼클래스 코드에서는 서브클래스에서 구현할 메소드를 호출해서 필요한 타입의 오브젝트를 가져와 사용한다. 이 메소드는 주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼클래스에서는 알지 못한다. 사실 관심도 없다. 서브클래스는 다양한 방법으로 오브젝트를 생성하는 메소드를 재정의할 수 있다. 이렇게 서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메소드팩토리 메소드라고 하고, 이 방식을 통해 오브젝트 생성 방법을 나머지 로직, 즉 슈퍼클래스의 기본 코드에서 독립시키는 방법을 팩토리 메소드 패턴이라고 한다.

 

이렇게 템플릿 메소드 패턴 또는 팩토리 메소드 패턴으로 관심사항이 다른 코드를 분리해내고, 서로 독립적으로 변경 또는 확장할 수 있도록 만드는 것은 간단하면서도 매우 효과적인 방법이다.

하지만 이 방법은 상속을 사용했다는 단점이 있다. 상속 자체는 간단해보이고 사용하기도 편리하게 느껴지지만 사실 많은 한계점이 있다.

   만약 이미 UserDao가 다른 목적을 위해 상속을 사용하고 있다면 어쩔 것인가? 자바는 클래스의 다중상속을 허용하지 않는다. 단지, 커넥션 객체를 가져오는 방법을 분리하기 위해 상속구조로 만들어버리면, 후에 다른 목적으로 UserDao에 상속을 적용하기 힘들다.

   또 다른 문제는 상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다는 점이다. 상속을 통해 관심이 다른 기능을 분리하고, 필요에 따라 다양한 변신이 가능하도록 확장성도 줬지만 여전히 상속관계는 두 가지 다른 관심사에 대해 긴밀한 결합을 허용한다.

서브클래스는 슈퍼클래스의 기능을 직접 사용할 수 있다. 그래서 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있다. 반대로 그런 변화에 따른 불편을 주지 않기 위해 슈퍼클래스가 더 이상 변화하지 않도록 제약을 가해야 할지도 모른다.

   확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다는 것도 큰 단점이다. 만약 UserDao이외의 DAO 클래스들이 계속 만들어진다면 그때는 상속을 통해서 만들어진 getConnection() 구현 코드가 매 DAO 클래스마다 중복돼서 나타나는 심각한 문제가 발생할 것이다.


1.3 DAO의 확장

모든 오브젝트는 변한다고 했다. 그런데 오브젝트가 다 동일한 방식으로 변하는 건 아니다. 관심사에 따라서 분리한 오브젝트들은 제각기 독특한 변화의 특징이 있다. 지금까지 데이터 엑세스 로직을 어떻게 만들 것인가와 DB 연결을 어떤 방법으로 할 것인가라는 두 개의 관심을 상하위 클래스로 분리시켰다. 이 두 개의 관심은 변화의 성격이 다르다.

변화의 성격이 다르다는 건 변화의 이유와 시기, 주기 등이 다르다는 뜻이다. 추상 클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꿔서 쓸 수 있게 만든 이유는 바로 이렇게 변화의 성격이 다른 것을 분리해서, 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서다. 그러나 여러가지 단점이 많은 상속이라는 방법을 사용했다는 사실이 불편하게 느껴진다.

 

- 클래스의 분리

처음에는 독립된 메소드를 만들어서 분리했고, 다음에는 상하위 클래스로 분리했다. 이번에는 아예 상속관계도 아닌 완전히 독립적인 클래스로 만들어본다.

* 독립시킨 DB 연결 기능인 SimpleConnectionMaker

public class SimpleConnectionMaker {

    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        return DriverManager.getConnection(
            "jdbc:mysql://localhost/study_toby?characterEncoding=UTF-8&serverTimezone=UTC",
            "toby", "7882"
        );
    }
}

* 수정

public class UserDao {

    private final SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        // 상태를 관리하는 것도 아니니 한 번만 만들어 인스턴스 변수에 저장해두고 메소드에서 사용하게 한다.
        simpleConnectionMaker = new SimpleConnectionMaker();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        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 {
        Connection c = simpleConnectionMaker.makeNewConnection();
        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;
    }
}

UserDao는 상속을 통한 방법을 쓰지 않으니 더 이상 abstract일 필요는 없다. 먼저 생성자에서 SimpleConnectionMaker의 오브젝트를 만들고, add/get 메소드에서 이를 이용해 DB 커넥션을 가져오면 된다.

기존 코드에 많은 수정을 했지만 기능에 변화를 준 것은 없다. 단지 내부 설계를 변경해서 좀 더 나은 코드로 개선했을 뿐이다.

기능에 변화가 없다는 것은 이런 리팩토링 작업의 전제이기도 하지만, 사실은 검증 내용이기도 하다. main()메소드를 다시 동작해서 테스트를 하자.

잘 동작한다.

... 포스팅 작성중

320x100

댓글