공부해봅시당

[스프링] 오브젝트와 의존관계 2 본문

STUDY/Spring

[스프링] 오브젝트와 의존관계 2

tngus 2022. 10. 14. 19:15

1.2. DAO의 분리

1.2.1. 관심사의 분리

개발자가 객체를 설계할 때 가장 염두에 둬야 할 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다.

(유지보수에 대한 생각을 하지 않고 현재에 급급해 개발을 하다보면 바로 스파게티코드 뚝딱ㅠ)

 

분리와 확장을 고려한 설계를 해야 미래의 변화에 대해 유연하고 효율적으로 대처할 수 있다.

 

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

 

1.2.2. 커넥션 만들기의 추출

UserDao의 구현된 메소드를 다시 살펴보자

 

UserDao의 관심사항

  • DB와 연결을 위한 커넥션
    • 어떤 DB를 쓸지
    • 어떤 드라이버를 사용할 것인지
    • 어떤 로그인 정보를 쓰는지
    • 그 커넥션을 생성하는 방법은 무엇인지
  • 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것
    • 주 관심사 > 파라미터로 넘어온 사용자 정보를 Statement에 바인딩시키고, Statement에 담긴 SQL을 DB를 통해 실행시키는 방법
      • 파라미터를 바인딩하는 것과 어떤 SQL을 사용할지를 다른 관심사로 분리할 수도 있지만 하나로 묶어서 생각해보자
  • 작업이 끝나면 사용한 리소스인 Statement와 Connection 오브젝트를 닫아줘서 소중한 공유 리소스를 시스템에 돌려주는 것

 

중복 코드의 메소드 추출

 

중복된 DB 연결 코드를 getConnection()이라는 이름의 독립적인 메소드로 만들어두도록 한다.

 

getConnection() 메소드를 추출해서 중복을 제거한 UserDao

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

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

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

 

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

코드 수정 후에는 기능에 문제가 없다는게 보장되지 않으므로 재검증이 필요하다.

 

현재 main() 코드에 적혀있는 id값은 DB에 저장되어 있으므로 에러를 일으킨다.

따라서 실행 전, DB를 초기화한 후 검증하길 바란다.

 

위 사항이 제대로 반영되었다면 문제없이 실행될 것이다.

 

현재 진행한 리팩토링 작업의 의의

  • 여러 메소드에 중복돼서 등장하는 특정 관심사항이 담긴 코드의 구조만 변경
  • 기능이 추가되거나 바뀐 것은 없지만 UserDao는 이전보다 훨씬 깔끔해짐(중복 코드의 제거)
  • 미래의 변화에 좀 더 손쉽게 대응 가능
현재 진행한 리팩토링 작업의 기법
- 메소드 추출(extract method) : 공통의 기능을 담당하는 메소드로 중복된 코드를 뽑아내는 것

 

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

장점
- 코드 내부의 설계가 개선되어 코드 이해가 용이해짐
- 변화에 효율적으로 대응 가능
=> 생산성 향상, 코드 품질 향상, 유지보수 용이, 견고하면서 유연한 제품 개발 가능

(리팩토링에 대해 더 자세히 알고 싶다면 [리팩토링 - 마틴 파울러, 켄트 벡 공저] 책을 추천한다)

 

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

UserDao 소스코드를 제공하지 않고도 DB 커넥션 생성 방식을 적용해가면서 UserDao를 사용하게 할 수 있을까?

 

상속을 통한 확장

위 물음의 해답은 "기존 UserDao 코드를 한 단계 더 분리하면 된다"는 것이다.

 

< 과정 >

  1. UserDao에서 메소드의 구현 코드 제거
  2. getConnection()을 추상 메소드 변경

 

상속을 통한 UserDao 확장 방법

 

UserDao

public abstract class UserDao {
	public void add(User user) throws ClassNotFoundException, SQLException {
    	Connection conn = getConnection();
        ...
    }
    
    public User get(String id) throws ClassNotFoundException, SQLException {
    	Connection conn = getConnection();
    }
    
    public abstract Connection getConnection getConnection() thrwos ClassNotFoundException, SQLException;
    
    public class NUserDao extends UserDao {
    	public Connection getConnection() throws ClassNotFoundException, SQLException {
        	// N 사 DB connection 생성코드
        }
    }
    
    public class DUserDao extends UserDao {
    	public Connection getConnection() throws ClassNotFoundException, SQLException {
        	// D 사 DB connection 생성코드
        }
    }
}

 

DAO의 핵심 기능

  • UserDao : 어떻게 데이터를 등록하고 가져올 것인가
    • SQL 작성, 파라미터 바인딩, 쿼리 실행, 검색정보 전달 등
  • NUserDao, DUserDao : DB연결 방법은 어떻게 할 것인가

위 두가지 핵심 기능이 클래스 계층구조를 통해 독립적으로 분리되면서 변경 작업이 용이해졌다.

이제 UserDao의 코드는 한 줄도 수정할 필요 없이 DB 연결 기능을 새롭게 정의한 클래스를 만들 수 있다.

더해, UserDao는 단순히 변경이 용이하다라는 수준을 넘어서 손쉽게 확장된다고 말할 수 있다.

 

디자인 패턴

소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션
모든 패턴에는 간결한 이름이 있어서 잘 알려진 패턴을 적용하고자 할 때 간단히 패턴 이름을 언급하는 것만으로도 설계의 의도와 해결책을 함께 설명 가능

디자인 패턴은 주로 객체자향 설계에 관한 것
대부분 객체지향적 설계 원칙을 이용해 문제 해결

패턴의 설계 구조가 대부분 비슷한 이유
> 객체지향적인 설계로부터 문제를 해결하기 위해 적용할 수 있는 확장성 추구 방법이 두 가지 구조로 정리되기 때문
1. 클래스 상속
2. 오브젝트 합성

따라서 패턴의 결과로 나온 코드나 설계 구조만 보면 대부분 비슷

패턴에서 가장 중요한 것은 각 패턴의 핵심이 담긴 목적 또는 의도
페턴을 적용할 상황, 해결해야 할 문제, 솔루션의 구조와 각 요소의 역할과 함께 핵심 의도가 무엇인지를 기억해둬야 함
템플릿 메소드 패턴(template method pattern) - 디자인 패턴 중 하나로 스프링에서 애용되는 디자인 패턴

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

<추가 설명>
상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법
변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 함
슈퍼클래스에서는 미리 추상 메소드 또는 오버라이드 가능한 메소드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메소드를 만듦

훅(hook) 메소드
슈퍼클래서에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드

서브클래스에서는 추상 메소드를 구현하거나, 훅 메소드를 오버라이드하는 방법을 이용해 기능의 일부 작성

 

팩토리 메소드 패턴(factory method pattern)

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

<추가 설명>
상속을 통해 기능을 확장하게 하는 패턴
슈퍼클래스 코드에서는 서브클래스에서 구현할 메소드를 호출해서 필요한 타입의 오브젝트를 가져와 사용
이 메소드는 주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼클래스에서는 알지 못함(관심도 없음)

서브클래스는 다양한 방법으로 오브젝트를 생성하는 메소드 재정의 가능

팩토리 메소드
서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메소드

팩토리 메소드 패턴
이 방식을 통해 오브젝트 생성 방법을 나머지 로직, 즉 슈퍼클래스의 기본 코드에서 독립시키는 방법

 

팩토리 메소드 패턴

 

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

 

하지만 상속을 사용했다는 단점

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

 

문제1. 자바는 클래스의 다중상속을 허용하지 않음

만약 이미 UserDao가 다른 목적을 위해 상속을 사용하고 있다면?

 

문제2. 상속을 통한 상하위 클래스의 관계는 생각보다 매우 밀접

상속을 통해 관심이 다른 기능을 분리하고, 필요에 따라 다양한 변신이 가능하도록 확장성도 줬지만 여전히 상속관계는 두 가지 다르관심사에 대해 긴밀한 결합을 허용

서브클래스는 슈퍼클래스의 기능을 직접 사용 가능

따라서 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있음

반대로 그런 변화에 따른 불편을 주지 않기 위해 슈퍼클래스가 더 이상 변화하지 않도록 제약을 가해야 함

 

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

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