본문 바로가기

Programming/Spring

[스프링] 오브젝트 설계와 제어의 역전

반응형

스프링은 자바를 기반으로 한 기술이자, 자바 엔터프라이즈 어플리케이션 개발에 사용되는 프레임워크이다. 어플리케이션 프레임워크는 어플리케이션 개발을 빠르고 효율적으로 할 수 있도록 어플리케이션의 바탕이 되는 틀과 공통 프로그래밍 모델, API 등을 지원해준다.

스프링이 자바에서 가장 중요하게 가치 두는 것은 객체지향 프로그래밍이 가능한 언어라는 점이다. 특히, 그 중에서도 오브젝트에 가장 많이 관심을 둔다.

어플리케이션에서 오브젝트가 생성되고, 다른 오브젝트와 관계를 가지는지 등은 스프링에서의 객체 지향에서 가장 중점적인 내용이다.


스프링은 다음 세 가지 핵심 프로그래밍 모델을 지원한다.

  1. IOC/ DI : 오브젝트의 생명 주기와 의존관계에 대한 프로그래밍 모델
  2. 서비스 추상화 : 환경이나 서버, 특정 기술에 종속되지 않고 이식성이 뛰어나고 flexible한 어플리케이션을 만들 수 있다.
  3. AOP : 어플리케이션 코드에 산재해서 나타나는 부가적인 기능을 독립저긍로 모듈화하는 프로그래밍 모델


DAO 설계와 분리, 확장

스프링의 오브젝트와 의존관계를 이해하는 것은 DAO를 설계하고 분리하는 단계에서부터 시작한다. 보통 사용자 정보를 JDBC API를 통해 DB에 저장하고 조회할 수 있는 DAO를 만들기 위해 자바빈 규약을 따르는 오브젝트를 이용한다. 

DAO : Data Access Object, DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트
자바빈 : 다음과 같은 두 가지 관례를 따라 만들어진 오브젝트를 가리킨다. 
  • 디폴트 생성자 : 자바빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다. 
  • 프로퍼티 : 자바빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 setter 와 getter 를 이용해 수정 또는 조회할 수 있다. 

모든 코드는 변경이 불가피하다. DB를 다루는 프로그램일수록 개발자는 코드의 변경과 확장에 대해 더 많이 고려해야하는 데, 코드의 변경이 일어날 때 필요한 작업을 최소화하고 영향 반경을 줄이려면 이를 고려한 설계가 필요하다. 
이를 위한 프로그래밍의 기초 개념 중 하나는 관심사의 분리이다. 관심사의 분리를 객체지향에 적용해보면, 관심이 같은 것 끼리는 하나의 객체 안으로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어지게(다른 클래스나 메소드로) 만들어서 서로 영향을 주지 않도록 하는 것이다. 

관심사의 분리를 적용하는 아주 기초적인 방법에는 중복코드의 메소드 추출이 있다.

이제 코드의 적극적인 변화 수용을 위해 알아두어야 할 개념들을 소개하겠다.

디자인 패턴

소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션. 주로 객체지향 설계에 관한 것이고, 대부분 객체 지향적 설계 원칙을 이용하여 문제를 해결한다. 

1) 탬플릿 메소드 패턴
상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 대표적인 방법이다. 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법이다. 보통 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만든다. 

길게 설명할 필요 없이, 코드를 통해 소개하겠다.

String 타입의 id, name, password 필드를 저장하는 User클래스가 같은 패키지 안에 있다고 가정한다. 이 User클래스는 자바빈 규약을 따르며 get, set메소드와 필드로만 이루어져있다. 


public abstract class UserDao {
public void add(User user) throws ClassNotFoundException, SQLException {
Connection c = getConnection()
; // Connection 인터페이스에 정의된 메소드 사용

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 = getConnection()
; // Connection 인터페이스에 정의된 메소드 사용

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

public abstract Connection getConnection() throws 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 생성 코드
}
}


슈퍼클래스에서는 기본적인 로직의 흐름을 담는다. 커넥션 가져오기, SQL 생성, 실행, 반환의 역할을 하고 있고, 이 중 DB 커넥션을 가져오는 기능을 추상 메소드로 만들어 상속을 통해 서브클래스로 분리하여 구현할 수 있게끔 만들었다. 

참고로 말하자면, 이 때 서브클래스 NUserDao와 DUserDao는 getConnection() 메소드가 언제 어떻게 사용되는 지, 자기자신은 모른다. 그건 서브클래스에 의해 결정되는 방식이 아니라 슈퍼클래스인 UserDao의 메소드에서 필요할 때 호출해서 사용하는 방식이다. 즉, 제어권을 상위 템플릿 메소드에 넘기고 자신은 필요할 때 호출되어 사용되도록한다는 제어의 역전 개념을 채택하였다. 이에 관한 설명은 밑에서 더 자세히 하겠다. 

2) 팩토리 메소드 패턴
위의 코드를 조금 다른 시각으로 보자면 UserDao의 서브클래스의 getConnection() 메소드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지를 결정하는 방법을 구현한다고도 볼 수 있다. 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴이라 한다. 
여기서 알아두어야 할 점은, UserDao는 Connection 오브젝트가 만들어지는 방법과 내부 동작에는 상관없이 자신이 필요한 기능을 Connection 인터페이스(인터페이스 오브젝트)를 통해 사용하기만 할 뿐이다. 

getConnection()에 들어갈 DB Connection 코드는 다음과 같다. 

Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook?useSSL=false", "spring", "book");

return c;
이처럼 DB Connection을 요청하는 코드를 많은 다른 회사들이 내놓는다면, 이같은 형식의 코드를 담은 DUserDao뿐만 아니라 NUserDao, GUserDao 등이 추가 확장될 것이다. 그렇다면 이와 같은 서브클래스의 getConnection()을 통해 만들어진 Connection 오브젝트의 종류나 갯수도 그에 맞추어 추가 생성되는 개념이다. 


팩토리 메소드 패턴도 템플릿 메소드 패턴과 마찬가지로 상속을 통해 기능을 확장하게 하는 패턴이다. 
슈퍼클래스(UserDao) 코드에서는 서브클래스에서 구현할 메소드(getConnection())를 호출해서 (Connection c = getConnection()) 필요한 타입의 오브젝트(c)를 가져와 사용한다.  이 때 이 메소드는 주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼클래스에서 알지 못한다. 서브클래스는 다양한 방법으로 오브젝트 생성하는 메소드를 재정의 할 수 있다.

이렇게 서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해 둔 메소드를 팩토리 메소드라 하고, 이 방식을 통해 오브젝트 생성방법을 슈퍼클래스의 기본 코드에서 독립시키는 방법을 팩토리 메소드 패턴이라 한다.  

즉 위의 코드는,
UserDao에 팩토리 메소드 패턴을 적용해서 getConnection을 분리한 것이다.

클래스 사이의 관계 vs 오브젝트 사이의 관계

한 프로그램에서 사용하는 두 클래스를 완전히 분리시켰다는 말의 의미를 먼저 생각해보자. 
의존 관계의 두 클래스, UserDao와 인터페이스 ConnectionMaker에서, UserDao는 ConnectionMaker를 이용하는 클래스이다. 이 때, ConnectionMaker를 구현하는 클래스들이 추가 확장되어도 UserDao를 건들이는 일이 아예 없다면? 두 클래스는 완전히 분리된 상황이다. 
객체지향 프로그래밍에서는 보통 이 같은 상황을 목표로 한다. 한 가지 기능을 추가/수정하자고 두 개 이상의 클래스를 대폭 수정하는 것은 최악이다. 

이렇게 두 클래스를 완전히 분리시키기 위한 첫 걸음으로서 클래스 사이의 관계가 아닌, 오브젝트 사이의 관계를 설정해주어야한다. 

클래스 사이의 관계 : 한 클래스가 인터페이스 없이 다른 클래스를 직접 사용한다는 뜻으로, 코드에 다른 클래스 이름이 나타난다. 
오브젝트 사이의 관계 : 코드에서는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있다. 객체지향 프로그램에서의 '다형성'이 이와 관계된 개념이다. 

개방 폐쇄 원칙(OCP)

클래스나 모듈은 확장에는 열려있어야 하고 변경에는 닫혀 있어야 한다. 인터페이스를 사용해 확장 기능을 정의한 대부분의 API는 바로 이 개방 폐쇄 원칙을 따른다. 이 때 동시에 클래스 내에서 자신의 핵심 기능을 구현한 코드(인터페이스로 나간 코드를 제외한 코드)는 인터페이스의 구현(변화)에 영향받지 않고 코드를 유지할 수 있으므로 변경에 닫혀있다고 말할 수 있다. 

개발 폐쇄 원칙은 높은 응집도와 낮은 결합도라는 소프트웨어 개발의 고전적인 원리로도 설명한다. 

전략 패턴

자신의 기능 맥락에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴. 이 때의 알고리즘이란 독립적인 책임으로 분리가 가능한 기능.


제어의 역전(IoC)

IoC의 개념을 먼저 소개하기 전에, '제어권'에 대해 설명하기 위해 코드를 보겠다. 위 코드의 완성 버전이라 보면 될 것 같다. 

코드를 실행하기 위해서는, MySQL에 springbook이라는 스키마를 하나 생성하여(id : spring, password : book으로 생성) 다음 명령어를 실행해 놓아야 한다는 점을 잊지말자. 

create table users (
id varchar(10) primary key,
        name varchar(20) not null,
        password varchar(10) not null
)

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package springbooks.user.domain;
 
public class User {
    String id;
    String name;
    String password;
 
    public void setId(String id){
        this.id = id;
    }
 
    public String getId() {
        return id;
    }
 
    public void setName(String name){
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setPassword(String password){
        this.password = password;
    }
 
    public String getPassword() {
        return password;
    }
}
 
cs



UserDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package springbooks.user.userdao;
 
import springbooks.user.domain.User;
import java.sql.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
 
public class UserDao {
    private ConnectionMaker connectionMaker; //밑에서 DI 받고 있으므로 특정클래스 타입이면 안됨. 인터페이스 타입.
 
    
    //의존관계 주입을 위한 코드 : 런타임 의존관계를 갖는 오브젝트를 인스턴스 변수에 저장해주기
    public UserDao(ConnectionMaker connectionMaker) {
        //오브젝트를 한 번만 만들어주기 위해 생성자에 넣어준 것.
        this.connectionMaker = connectionMaker;
        //생성자를 통해 DConnectionMaker 오브젝트 간접 전달 (직접 전달하면 코드 수정이 불가피)
    }
    
    public void add(User user) throws ClassNotFoundException, SQLException{
        Connection c = connectionMaker.makeConnection();
        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 = connectionMaker.makeConnection();
 
        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;
    }
}
 
//db연결방식을 정의할 수 있는 틀 : interface
interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
 
    //고객이 구현할 자신들의 DB연결 코드 -> 전략을 바꿔가면서 사용할 수 있게끔
    class DConnectionMaker implements ConnectionMaker {
        public Connection makeConnection() throws ClassNotFoundException, SQLException{
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook?useSSL=false""spring""book");
 
            return c;
        }
}
 
 
//내가 만든 ConnectionMaker 구현 클래스를 어떤 것을 사용할 지를 결정한 권한을 가진 클래스
//UserDao는 이제 수동적인 존재가 됐음 -> ConnectionMaker를 사용하지만 제어하는 기능이 없음 : 제어의 역전
 
class DaoFactory {
    public UserDao userDao() {
        //UserDao가 사용할 오브젝트를 DaoFactory에서 공급해주고 있음
        return new UserDao(connectionMaker()); //파라미터로 오브젝트의 레퍼런스 전달
        
    }
 
    //DI컨테이너 자신이 결정한 의존관계를 맺어줄 클래스의 오브젝트를 만드는 코드
    public ConnectionMaker connectionMaker() {
        return new DConnectionMaker();
    }
}
 
 
cs


MainTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package springbooks.user.userdao;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import springbooks.user.domain.User;
import java.sql.*;
 
//ConnectionMaker와 UserDao을 연결시켜주는 제 3의 클라이언트 클래스
public class MainTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        UserDao dao = new DaoFactory().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(user.getPassword());
        System.out.println(user2.getId() + " 조회 성공");
    }
}
 
cs

위 코드는 이제부터 설명할 개념을 모두 적용시켜놓은 완성본이다. 그럼 이제 코드를 이해하기 시작해보자.

여기서 주목할 코드들은 UserDao의 UserDao(ConnectionMaker connectionMaker), ConnectionMaker, DaoFactory, 그리고 MainTest이다. 
사실 여기서 UserDao.java의 DaoFactry 클래스가 하는 일이 그렇게 복잡하지도 않은데 MainTest에서 책임하지 않은 이유가 무엇일까?
위에서도 강조했듯, 모든 클래스는 한가지 일에만 관심을 두고 있어야한다. 만약 한 클래스가 두개 이상의 일을 하고 있다면 분리시키는 게 맞다. MainTest를 그저 테스트의 업무만 담당하게 만들기 위해, DaoFactory 라는 클래스를 하나 만들었다. 

DaoFactory에서 하는, 객체 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 일을 하는 오브젝트팩토리(factory)라고 부른다. 이 팩토리는 애플리케이션을 구성하는 컴포넌트의 구조와 관계를 정의한 설계도 같은 역할을 하고 있다고 보면 된다. 어떤 오브젝트가 어떤 오브젝트를 사용할 지를 정의해놓았기 때문이다.


제어의 역전

프로그램에서의 제어 흐름 개념을 거꾸로 뒤집는 것이다. 일반적인 제어흐름이란, main() 메소드와 같이 프로그램이 시작되는 지점에서 다음에 사용할 오브젝트를 결정하고, 결정한 오브젝트를 생성하고, 만들어진 오브젝트에 있는 메소드를 그 안에서 직접 호출하고, 그 메소드는 또 그 안에서 자기가 다음에 사용할 것을 직접 결정하고 호출하는 식의 작업이 반복되는 것이다. 각 클래스는 사용할 오브젝트를 호출하는 등의 구성 작업을 능동적으로 한다. 

제어의 역전에서는 오브젝트(클래스)가 자신이 사용할 오브젝트를 스스로 선택하지도, 생성하지도 않는다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임한다. 프로그램의 시작을 담당하는 main()과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 이렇게 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다. 
 
프레임워크는 제어의 역전 개념이 적용된 대표적인 기술이다. '제어'의 개념을 중심으로, 프레임워크라이브러리의 차이를 정리하겠다.
라이브러리 : 애플리케이션 코드는 필요한 기능이 있을 때 능동적으로 라이브러리를 사용한다. 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션의 흐름을 직접 제어한다.
프레임워크 : 애플리케이션 코드는 프레임워크에 의해 사용된다. 프레임워크 위에 개발한 클래스를 등록해두고, 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만든다. 애플리케이션 코드는 프레임워크가 짜놓은 틀에서 수동적으로 동작하므로 프레임워크에는 분명한 제어의 역전 개념이 적용되어있다. 



UserDao와 DaoFactory에서도 제어의 역전 개념이 적용되어 있다. UserDao는 자신이 어떤 ConnectionMaker 구현 클래스를 생성하고 사용할지 결정할 권한을 DaoFactory에 넘기고, 수동적인 존재가 됐다. UserDao dao = new DaoFactory().userDao(); 에서 보이듯, 자기 자신도 팩토리에 의해 수동적으로 만들어지고 자신이 사용할 오브젝트도 DaoFactory에서 공급해주고 있다. MainTest는 DaoFactory가 만들고 초기화해서 자신에게 사용하도록 공급해주는 ConnectionMaker를 사용할 수 밖에 없다. 또한 UserDao와 ConnectionMaker의 구현체도 DaoFactory가 생성하고 있다. 

이렇게 코드와 함께 이해해보면 제어의 역전 개념이 확 와닿을 것이다. 

코드를 보며 느꼈겠지만, 제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요하다. 이 프로그램처럼 단순한 IoC 적용이라면 DaoFactory같은 컨테이너로도 프로그램이 잘 돌아가겠지만, IoC를 애플리케이션 전반에 걸쳐 사용하기 위해서는 스프링 같은 프레임워크의 도움을 받는 것이 훨씬 좋다. 

이번 포스팅에서는 스프링으로의 도입을 위한 기반 개념들을 소개하였다. 다음 포스팅은 이번 포스팅의 후편이 될 것이므로, 이번 포스팅에서의 코드를 그대로 가져가겠다. 








반응형