[토비의 스프링] 오브젝트 설계하기

민주·2022년 8월 16일
0

spring framework

목록 보기
1/1
post-thumbnail

1. 객체지향 프로그래밍

스프링이 자바에서 가장 중요하게 가치를두는 것은 객체지향 프로그래밍이다. 스프링에서 관심을 많이 가지는 것 또한 오브젝트이기 때문에, 핵심 기술을 제대로 활용하려면 객체지향 설계 원칙을 이해하고 지켜야 한다.

1-1 관심사의 분리

사용자의 정보를 관리하는 UserDao 클래스가 있다. 이 클래스가 user 테이블의 CRUD를 수행하기 위해서는 먼저 DB 커넥션을 가져와야 한다. 만약 DB와 연결하는 작업을 공통으로 분리하지 않고 각각의 메서드에 작성하게 된다면 코드는 아래처럼 된다.

public class UserDao {
	// insert user
	public void add(User user) throws ClassNotFoundException, SQLException {
    	Class.forName(JDBC_DRIVER);  // postgresql, oracle..
        Connection c = DriverManager.getConnection(DB_URL, ID, PW);	 // DB 커넥션 생성
		PreparedStatement ps = c.prepareStatement(
			"insert into users(id, name, password) values(?,?,?)");  // Statement에 query 셋팅
		ps.setString(1, user.getId()); ps.setString(2, user.getName()); 
    	ps.setString(3, user.getPassword());
		ps.executeUpdate();  // Statement 실행
		ps.close();
		c.close();  // 리소스 닫아주기
    }
    
    // select user
    public User get(String id) throws ClassNotFoundException, SQLException {
    	Class.forName(JDBC_DRIVER);
		Connection c = DriverManager.getConnection(DB_URL, ID, PW);
		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가 사용자 정보 테이블과 관련된 기능에 집중하고 있어야 하는데, DB 커넥션과 관련된 기능까지 모든 메서드에서 구현하고 있다.

add, get 메서드 이외에도 UserDao에 사용자 정보를 수정하고 삭제하는 update, delete 메서드를 추가하게 된다면 위 메서드에서 구현했던 DB커넥션 기능까지 전부 반복해서 작성해야 하는 불편함이 생긴다.

이 문제를 해결하기 위해서는 관심사의 분리가 필요하다. 객체지향 프로그래밍이 절차적 프로그래밍에 비해 초기에 더 많은 번거로운 작업을 요구하는 이유는 객체지향 기술이 변화에 효과적으로 대처할 수 있는 기술적인 특징이 있기 때문이다.

객체지향 기술은 실세계를 최대한 비슷하게 추상세계로 모델링 할 수 있고, 이를 효과적으로 구성하고 변경, 발전, 확장시킬 수 있다. 이러한 객체지향 기술로 변화의 폭을 최소한으로 줄여줄 수가 있는데, 그것은 분리와 확장을 고려한 설계가 필요하다.

개발자는 한 가지 관심이 한 군데 집중되게 해야한다. 관심사가 같은 것끼리 모으고 다른것은 분리해줌으로써 같은 관심에 효과적으로 집중할 수 있게 만들어주는 것이다.

1-2 UserDao의 관심사항

위 add, get 메서드는 크게 DB 연결과 관련된 기능, DB에 보낼 SQL을 담은 Statement를 만들고 실행하는 기능, 작업을 끝낸 뒤 사용한 리소스를 닫아주는 기능으로 나눌 수 있다.

현재의 UserDao에서는 DB 커넥션을 가져오는 코드가 여기저기 중복돼서 나타날 것이다.
이렇게 하나의 기능이 여기저기 흩어져 다른 비즈니스 로직과 얽혀 있으면 유지보수의 어려움이 있기 때문에, 중복이 되는 코드는 메서드로 추출해야 한다.

1-3 중복 코드의 메서드 추출

중복된 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 getConnection() throws ClassNotFoundException, SQLException {
    	Class.forName("com.mysql.jdbc.Driver");
		Connection c = DriverManager.getConnection(DB_URL, ID, PW);
		return c;
	}

이렇게 DB 연결 기능을 따로 메서드를 분리해서 만들게 되면 드라이버 클래스나 URL, 로그인 정보가 변경 되었을 때, 이 메서드 한 번만 수정하면 된다. 관심이 다른 코드가 있는 메서드에 영향을 주지도 않고, 독립적으로 존재해서 수정도 간단해졌다.

1-4 상속을 통한 확장

요구사항에 따라 좀 더 변화에 열려있는 DAO가 필요할 수 있다. 하나의 DAO를 사용하는 클라이언트가 여럿 일 때, 클라이언트 마다 각기 다른 종류의 DB를 사용하고 있어 DB 커넥션을 가져오는데 독자적인 방법을 적용하고 싶어한다면 위 방법으로는 요구사항을 만족할 수 없다.

이럴때는 상속을 통해 확장성을 높혀야 한다. UserDao를 한 번 더 분리하기 위해 메서드 구현 코드를 제거하고, getConnection()을 추상 메서드로 만든다.

이제 이 추상 클래스 UserDao를 사용자들에게 전달하면, 이들은 UserDao 클래스를 상속하는 서브 클래스를 만든다. 서브 클래스에서는 UserDao에서 추상 메서드로 선언했던 getConnection() 메서드를 원하는 방식으로 구현할 수 있다.

이렇게 하면 UserDao의 소스코드를 제공해서 수정해 쓰도록 하지 않아도 getConnection() 메서드를 원하는 방식으로 확장해서 다른 UserDao의 기능과 함께 사용할 수 있다.

기존에 메서드로 분리했던 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 ClientADao extends UserDao {
	// getConnection 구현
	public Connection getConnection() throws ClassNotFoundException, SQLException {
		// A Client DB connection 생성코드
	}
}
public class ClientBDao extends UserDao {
	// getConnection 구현
	public Connection getConnection() throws ClassNotFoundException, SQLException {
		// B Client connection 생성코드
	}
}	

이렇게 수정한 코드는 데이터를 등록하고 가져오는 방법을 담당하는 UserDao와, DB 연결 방법을 담당하는 ClientADao, ClientBDao가 클래스 레벨로 구분되어있다.

클래스 계층구조를 통해 두 개의 관심이 독립적으로 분리되면서 변경 작업은 간단해졌다. 새로운 DB 연결 방법을 적용해야 할 때는 UserDao를 상속을 통해 확장해주면 된다.

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

이 방식은 또한 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 한다고 볼 수 있는데 이 것을 팩토리 메서드 패턴(Factory method pattern) 이라고 부르기도 한다.


si에서 일하는 동안에는 항상 기한 맞추기에 급급했다. 찍어내듯이 api를 만들고 페이지를 만들어서 개발의 매너리즘에 빠진 것 같았다. 프로젝트에 참여한 개발자들은 각각 코딩 스타일이 중구난방이었고 공통을 손 볼 시간은 없었다. 개발 기간임에도 불구하고 프로젝트가 레거시가 되는 걸 지켜볼 수밖에 없었다. 다시는 이런 방식으로 일을 하고 싶지 않았고, 앞으로는 요구사항에 맞게 스프링 핵심 기술을 꼭 적용해야겠다는 생각이 들었다.

그래서 퇴사한 후, 스프링을 개념부터 다시 공부하고 있다. 잊고 있었던 개념부터 다시 정리하기 시작하면서 평소에 많이 활용할 기회가 없었던 스프링 고급 기술까지 공부해 보려고 한다. 다시 기간이 빠듯한 프로젝트에 투입이 되더라도 같은 실수를 반복하지 않도록 스프링에 익숙해져야겠다.

0개의 댓글