모래블로그

토비의 스프링 3.1 정리(1) 본문

독서 및 정리/토비의 스프링

토비의 스프링 3.1 정리(1)

별모래 2023. 11. 28. 23:07
728x90

1장 오브젝트와 의존관계

1) 초난감 DAO

  • DAO(Data Access Object) : DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트
  • 자바빈(Java Bean) : 원래 비주얼 툴에서 조작 가능한 컴포넌트를 말한다. 자바의 주력 개발 플랫폼이 웹 기반의 엔터프라이즈 방식으로 바뀌면서 비주얼 컴포넌트로서 인기를 잃어갔지만, 자바빈의 몇 가지 코딩 관례는 JSP 빈, EJB와 같은 표준 기술과 자바빈 스타일의 오브젝트를 사용하는 오픈소스 기술을 통해 계속 이어져왔다. 이제는 자바빈이라고 말하면 비주얼 컴포넌트라기 보다는 다음 두 가지 관례를 따라 만들어진 오브젝트를 가리킨다. 간단히 빈이라고 부르기도 한다.
    • 디폴트 생성자 : 자바빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다. 툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 때문에 필요하다.
    • 프로퍼티 : 자바빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 set으로 시작하는 수정자 메소드(setter)와 get으로 시작하는 접근자 메소드(getter)를 이용해 수정 또는 조회할 수 있다.

2) JDBC 를 이용하는 작업의 일반적인 순서

  • DB 연결을 위한 Connection을 가져온다
  • SQL을 담은 Statement(or PreparedStatement)를 만든다
  • 만들어진 Statement를 실행한다
  • 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다
  • 작업 중에 생성된 Connection, Statement ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다
  • JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다

UserDao 클래스

package springbook.user.dao;
...
public class UserDao {
	// 새로운 사용자 생성
	public void add(User user) throws ClassNotFoundException, SQLException {
    	Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        
        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.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        
        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;
    }

3) main()을 이용한 DAO 테스트 코드

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

User user = new User();
user.setId("morae1234");
user.setName("모래");
user.setPassword("iwanttogohome");

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() + " 조회 성공");
}
 

실행 결과

moare1234 등록 성공
모래
iwanttogohome
morae1234 조회 성공
 

위 코드는 초난감 코드의 조건을 두루 갖춘 DAO 코드다.

 


1.2 DAO의 분리

1) 관심사의 분리

객체지향 설계와 프로그래밍이 이전의 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 많은 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다. 객체지향 기술이 만들어내는 가상의 추상세계 자체를 효과적으로 구성할 수 있고, 이를 자유롭고 편리하게 변경, 발전, 확장시킬 수 있다는 데 더 의미가 있다.

  • 관심사의 분리 : 관심사가 같은 것끼리 모으고 다른 것은 분리해줌으로써 같은 관심에 효과적으로 집중할 수 있게 만들어준다.

2) 커넥션 만들기의 추출

UserDao의 관심사항

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

2. 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것. 여기서의 관심은 파라미터로 넘어온 사용자 정보를 Statement에 바인딩시키고, Statement에 담긴 SQL을 DB를 통해 실행시키는 방법

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

 

위의 DAO 코드에는 예외상황에 대한 처리가 전혀 없다. 수많은 동시 사용자가 있고, 한번 시작되면 장기간 운용되는 서버에 올라간다면 예외상황에 적절하게 대응해서 공유 리소스를 반환하지 않는 일이 없도록 세심하게 주의해야 한다.

 

 

 일단, 가장 문제가 되는 것은 첫째 관심사인 DB 연결을 위한 Connection 오브젝트를 가져오는 부분이다. 현재 DB 커넥션을 가져오는 코드는 다른 관심사와 섞여서 같은 add() 메소드에 담겨있다. 또한 add() 메소드에 있는 DB 커넥션을 가져오는 코드와 동일한 코드가 get() 메소드에도 중복되어 있다. 앞으로 수백, 수천 개의 DAO 메소드를 만들게 된다면 여기저기 중복되어 나타날 것이다.
 바로 이렇게 하나의 관심사가 방만하게 중복되어 있고, 여기저기 흩어져 있어서 다른 관심의 대상과 얽혀 있으면, 변경이 일어날 때 엄청난 고통을 일으키는 원인이 된다.
=> 지저분하게 꼬여 있는 스파게티 코드가 됨

 

중복 코드의 메소드 추출

가장 먼저 할 일은 커넥션을 가져오는 중복된 코드를 분리 => getConnection() 이라는 이름의 독립적인 메소드로 만들어둠

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

public User get(String id) throws ClassNotFoundException, SQLException {
	Connection c = getConnection();
}	

// DB 연결 기능이 필요하면 getConnection() 메소드를 이요하게 함
// 중복되는 코드를 독립적인 메소드로 만들어서 중복 제거
private Connection getConnection() throws ClassNotFoundException, SQLException {
	Class.forName("com.mysql.jdbc.Driver");
	Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
    return c;
}

 

 메소드가 수천개 일 때, DB 연결과 관련된 부분에 변경이 일어났을 경우, getConnection() 이라는 한 메소드의 코드만 수정하면 된다.

 관심의 종류에 따라 코드를 구분해놓았기 때문에 한 가지 관심에 대한 변경이 일어날 경우 그 관심이 집중되는 부분만 코드를 수정하면 된다. 관심이 다른 코드가 있는 메소드에는 영향을 주지도 않을 뿐더러, 관심 내용이 독립적으로 존재하므로 수정도 간단해졌다.

 

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

앞에서 이미 UserDao의 기능이 잘 동작한다는 것을 테스트해봤지만 코드를 수정한 후 기능에 문제가 없다는 게 보장되지 않는다. 다시 검증이 필요하다. 변경된 UserDao의 기능이 변경하기 전과 동일한지 확인해보려면 어떻게 해야 할까?

=> 앞에서 만들어뒀던 main() 메소드를 이용한 테스트를 실행해보면 된다.

단, 현재 main() 메소드 테스트에는 메소드를 여러 번 실행하면 두 번째부터 무조건 예외가 발생하므로(기본키 id 값이 중복) 메소드 테스트를 다시 실행하기 전에 User 테이블의 사용자 정보를 모두 삭제해줘야함

 

 

 방금 한 작업은 UserDao 기능에 아무런 변화를 주지 않았다. 여전히 사용자 정보를 등록하고 조회하는 조금 난감한 DAO 클래스일 뿐.
 하지만 중복돼서 등장하는 특정 관심사항이 담긴 코드를 별도의 메소드로 분리해냈고, 기능에는 영향을 주지 않고 코드의 구조만 변경했다. 기능이 추가되거나 바뀐 것은 없지만 UserDao는 훨씬 깔끔해졌고 미래의 변화에 손쉽게 대응할 수 있는 코드가 됐다. 이런 작업을 리팩토링 이라고 한다.

 

리팩토링은 객체지향 개발자라면 반드시 익혀야하는 기법이다.

  • 리팩토링 : 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술을 말한다.
    리팩토링을 하면 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고, 변화에 효율적으로 대응할 수 있다.
    결국 생산성은 올라가고, 코드의 품질은 높아지며, 유지보수하기 용이해지고, 견고하면서도 유연한 제품을 개발할 수 있다.

DB 커넥션 만들기의 독립

 이 UserDao를 구매하겠다는 주문이 들어왔다고 상상해보자.
 그런데 납품 과정에서 문제가 발생했다. N사와 D사가 각기 다른 DB를 사용하고 있고, DB 커넥션을 가져오는 데 있어 독립적으로 만든 방법을 적용하고 싶어한다는 점이다. 더 큰 문제는 UserDao를 구매한 이후에도 DB 커넥션을 가져오는 방법이 종종 변경될 가능성이 있다는 점이다.

=> 아예 UserDao 소스코드를 고객에게 제공해주고, 변경이 필요하면 getConnection() 메소드를 수정해서 사용하라고 할 수 있지만, 비밀기술이 적용되어 고객에게 소스를 공개하고 싶지 않다. 고객에게는 미리 컴파일된 클래스 바이너리 파일만 제공하고 싶다.
과연 이런 경우 UserDao 소스 코드를 N사와 D사에 제공해주지 않고도 고객 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDao를 사용하게 할 수 있을까?

 

 

상속을 통한 확장

일단 우리가 만든 UserDao 에서 메소드의 구현 코드를 제거하고 getConnection()을 추상 메소드로 만들어놓는다. 추상 메소드라서 메소드 코드는 없지만 메소드 자체는 존재한다. 따라서 add(), get() 메소드에서 getConnection() 을 호출하는 코드는 그대로 유지할 수 있다.

이제 이 추상 클래스인 UserDao를 N사와 D사에 판매한다. UserDao를 구입한 포탈사들은 UserDao 클래스를 상속해서 각각 NUserDao와 DUserDao라는 서브클래스를 만든다. 서브클래스에서는 UserDao에서 추상 메소드로 선언했던 getConnection() 메소드를 원하는 방식대로 구현할 수 있다. 이렇게 하면 UserDao의 소스코드를 제공해서 수정해 쓰도록 하지 않아도 getConnection() 메소드를 원하는 방식으로 확장한 후에 UserDao의 기능과 함께 사용할 수 있다.

 

상속을 통한 UserDao 확장 방법

 

 

리팩토링한 코드

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;
}

// 상속을 통해 확장된 getConnection() 메소드
public class NUserDao extends UserDao {
	pulbic Connection getConnection() throws ClassNotFoundException, SQLException {
    	// N사 DB connection 생성코드 
    }
}

public class DUserDao extends UserDao {
	pulbic Connection getConnection() throws ClassNotFoundException, SQLException {
    	// D사 DB connection 생성코드 
    }
}

 

DAO의 핵심 기능인 어떻게 데이터를 등록하고 가져올 것인가(sql 작성, 파라미터 바인딩, 쿼리 실행, 검색정보 전달) 라는 관심을 담당하는 UserDao와,

DB 연결 방법은 어떻게 할 것인가라는 관심을 담고 있는 NUserDao, DUserDao가 클래스 레벨로 구분이 되고 있다.

 

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

=> 이제 UserDao는 단순히 변경이 용이하다라는 수준을 넘어서 손쉽게 확장된다라고 말할 수도 있게 됐다. 새로운 DB 연결 방법을 적용해야 할 때는 UserDao 상속을 통해 확장해주기만 하면 된다.

 

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

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

 

UserDao에 적용된 팩토리 메소드 패턴

 

  • 디자인 패턴 : 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션. 
    모든 패턴에는 간결한 이름이 있어, 잘 알려진 패턴을 적용하고자 할 때 간단히 패턴 이름을 언급하는 것만으로도 설계의 의도와 해결책을 함께 설명할 수 있다는 장점이 있다. 디자인 패턴은 주로 객체지향 설계에 관한 것이고 대부분 객체지향적 설계 원칙을 이용해 문제를 해결한다.
     패턴의 설계 구조를 보면 대부분 비슷한데, 그 이유는 객체지향적인 설계로부터 문제를 해결하기 위해 적용할 수 있는 확장성 추구 방법이 대부분 두 가지 종류로 정리되기 떄문이다. 하나는 클래스 상속이고, 다른 하나는 오브젝트 합성이다.
     따라서 패턴의 결과로 나온 코드나 설계 구조만 보면 대부분 비슷해 보인다. 패턴에서 가장 중요한 것은 각 패턴의 핵심이 담긴 목적 또는 의도다. 패턴을 적용할 상황, 해결해야 할 문제, 솔루션의 구조와 각 요소의 역할과 함께 핵심 의도가 무엇인지 기억해둬야 한다.

 

  •  템플릿 메소드 패턴 : 상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능은 슈퍼클래스에 만들어두고, 자주 변경되며 확장할 기능은 서브 클래스에서 만들도록 한다.
     슈퍼클래스에서는 미리 추상 메소드 또는 오버라이드 가능한 메소드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메소드를 만든다. 슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드 할 수 있도록 만들어둔 메소드를 훅(hook) 메소드라고 한다.
     서브클래스에서는 추상 메소드를 구현하거나, 훅 메소드를 오버라이드하는 방법을 이용해 기능의 일부를 확장한다.
public abstract class Super {
	// 기본 알고리즘 골격을 담은 메소드를 템플릿 메소드라고 함
    // 템플릿 메소드는 서브클래스에서 오버라이드 하거나 구현할 메소드 사용
	public void templateMethod() {
    	// 기본 알고리즘 코드
        hookMethod();
        abstractMethod();
        ...
    }
    // 선택적으로 오버라이드 가능한 훅 메소드
    protected void hookMethod() { }
    // 서브클래스에서 반드시 구현해야하는 추상 메소드
    public abstract void abstractMethod();
}
    
// 슈퍼클래스의 메소드를 오버라이드 하거나 구현해서 기능을 확장
public class Sub1 extends Super{
    protected void hookMethod() {
        ...
    }
    public void abstractMethod() {
        ...
    }
}

 

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

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

상속을 사용했다는 단점이 있다.

 

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

만약 이미 UserDao가 다른 목적을 위해 상속하고 있다면 어쩔 것인가?

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

 

또 다른 문제는 상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다는 점이다.

상속을 통해 관심이 다른 기능을 분리하고, 필요에 따라 다양한 변신이 가능하도록 확장성도 줬지만 여전히 상속관계는 두 가지 다른 관심사에 대해 긴밀한 결합을 허용한다. 서브클래스는 슈퍼클래스의 기능을 직접 사용할 수 있다.

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

 

확장의 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다는 것도 큰 단점이다. 

만약 UserDao 외의 DAO 클래스들이 계속 만들어진다면 그 때는 상속을 통해서 만들어진 getConnection() 의 구현 코드가 매 DAO 클래스마다 중복돼서 나타나는 심각한 문제가 발생할 것이다.

 

Reference 
토비의 스프링 3.1
728x90