-
토비의 스프링[1] 오브젝트와 의존관계Spring 2020. 12. 17. 01:20
서론
관심사 분리하여 DAO를 처리하는 방법과 싱글톤 패턴 및 애플리케이션 컨텍스트에 대해 알아보며, 제어의 역전(IoC)에 대해 알아보자.
관심사 분리
초난감 DAO 처리하기
public class UserDao { public void add(User user) throws SQLException, ClassNotFoundException { Class.forName("com.mysql.cj.jdbc.Driver"); Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/sys?serverTimezone=UTC&characterEncoding=UTF-8", "root", "1234"); PreparedStatement ps = c.prepareStatement( "insert into users(id, name, pwd) values(?, ?, ?)"); ps.setString(1, user.getName()); ps.setString(2, user.getId()); ps.setString(3, user.getPwd()); ps.executeUpdate(); ps.close(); c.close(); } public User get(String id) throws SQLException, ClassNotFoundException { Class.forName("com.mysql.cj.jdbc.Driver"); Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/sys?serverTimezone=UTC&characterEncoding=UTF-8", "root", "1234"); 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.setPwd(rs.getString("pwd")); rs.close(); ps.close(); c.close(); return user; } } @Slf4j public class MainClass { public static void main(String[] args) throws SQLException, ClassNotFoundException { UserDao dao = new UserDao(); User user = new User(); user.setId("전인우"); user.setName("전인우"); user.setPwd("1234"); dao.add(user); log.info("user : {}", user.toString()); User user2 = dao.get(user.getId()); log.info("user2 : {}", user2.toString()); } }
DAO 분리
관심사의 분리(SoC)
관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이라고 생각할 수 있다.
커넥션 추출
DB연결 같이 중복되는 코드는 메소드를 만들어 호출하여 사용해서 중복되는 코드를 줄일 수 있다.
이러한 변경사항이 존재하더라도 Dao의 기능에는 영향을 주지 않으며 코드 구조만 변경 되었다. 이렇게 메소드로 분리하여 리팩토링 한 것 자체로도 훨씬 가독성이 좋아지며, 유지보수가 쉬운 코드를 만들 수 있다. (메소드 추출 기법)
상속을 이용한 확장
요구사항에 맞게 프로그램이 확장될 수 있도록 추상클래스를 이용하여 리팩토링이 가능하다.
서로 다른 DB를 사용해야 할 경우 각각의 서브클래스에서 각자 사용하는 DB만 변경이 가능하다.
슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이 메소드를 필요에 맞게 구현해서 사용할 수 있다. 이러한 방법 또한 디자인패턴중 템플릿 메소드 패턴이라고 불린다.
DAO 확장
위에서의 확장을 위하여 추상 클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꿔서 사용할 수 있게끔 성격이 다른 것들을 분리하여 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 도움을 주지만 상속이라는 제약이 어느정도의 발목을 잡는다.
클래스의 분리
-
관심사가 다르며, 변화의 성격이 다른 코드를 분리하기 위하여 클래스를 나누어 DB연결 클래스를 제작 후 사용이 가능하다. 하지만 코드를 나눈 행동은 괜찮지만 상속을 이용하여 요구사항에 맞도록 확장을 했던 기능이 사라졌다.
-
그러면 DB메소드를 계속 만들어주고 필요한 DB메소드에 맞게 클래스를 불러 호출하면 되잔아 -
그럴경우 해당 클래스는 무슨일을 하는 메소드가 존재하는지 등 필요없는 정보들을 알고 있어야 한다.
-
인터페이스의 도입
-
서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어서 분리가 가능하다. 자바에서의 추상화는 공통적인 성격을 뽑아내 이를 따로 분리하는 것이며 이에 적합한것이 바로 인터페이스이다.
-
인터페이스는 어떤 일을 하겠다는 기능만 정의를 하기에 실제 구현은 상속을 받아 사용하는 클래스에서 구현이 이루어 진다. (디폴트 메소드 제외) 그렇기에 기능의 내용은 관심을 가지지 않아도 된다.
-
관심을 인터페이스를 사용해서 분리를 하였지만 DAO에서 DB를 연결할때 인터페이스를 상속받아 기능을 구현한 클래스를 new로 생성하여 사용을 해야한다. 이러한 코드는 new라는 짧은 코드지만 그 자체로 충분히 독립적인 관심사를 가지고 있다.
-
불필요한 의존성을 제거하기 위해 DAO에서는 생성자를 이용하여 DB연결 인터페이스를 주입받고, 새로운 클래스를 생성하여 사용할 DB연결 클래스를 DAO에게 넘겨주도록 코드를 수정하면 DAO에는 어떤 DB연결 클래스를 사용했는지 관심사를 분리할 수 있다.
-
이렇게 잘 설계한 객체지향 클래스의 구조를 살펴보면 개방 폐쇄 원칙을 잘 준수하고 있다. 인터페이스를 사용해 확장 기능을 정의한 대부분의 API는 바로 이 개방 폐쇄 원칙(SOLID)을 따른다고 볼 수 있다.
-
SPR(The Single Responsibility Principle): 단일 책임 원칙
-
OCP(The Open Closed Principle): 개방 폐쇄 원칙
-
LSP(The Liskov Subsitution Principle): 리스코프 치환 원칙
-
ISP(The Interface Segregation Principle): 인터페이스 분리 원칙
-
DIP(The Dependency Inversion Principle): 의존관계 역전 원칙
높은 응집도
응집도가 높다 == 해당 모듈에서 변하는 부분이 크다
낮은 결합도
책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결합도, 느슨한 연결고리를 유지해야 독립적이게 된다. (간섭이 적어지고 A를 수정하더라도 B를 수정할 필요가 없어짐)
Spring IoC
스프링에서 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(been) 이라고 부른다. 자바빈 또는 엔터프라이즈 자바빈에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 말한다. 동시에 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말이다.
스프링에서 빈의 생성, 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(been factory)라고 부르며 이를 확장한 애플리케이션 컨텍스트(application context)를 주로 사용한다.
주입방법
XML 파일을 이용하여 넣기
@Configuration 및 @Bean 어노테이션을 이용하여 java파일로 넣기
스프링 IoC의 용어
빈 : Spring IoC에서 관리하는 오브젝트
빈 팩토리 : IoC를 담당하는 핵심 컨테이너(빈 등록, 생성, 조회 및 반환)
애플리케이션 컨텍스트 : 빈 팩토리를 확장한 IoC 컨테이너
설정정보/설정 메타정보 : 애플리케이션 컨텍스트 및 빈 팩토리가 IoC를 적용하기 위한 메타정보
컨테이너 또는 IoC 컨테이너 : IoC 방식으로 빈을 관리한다는 의미
싱글톤
DaoFactory를 직접 사용하는 것과 @Configruation 어노테이션을 추가하여 스프링 애플리케이션 컨텍스트를 통하여 사용하는 것은 테스트 결과만 보면 동일해 보인다. 하지만 실제로 스프링 애플리케이션 컨텍스트와 DaoFactory에서의 UserDao() 메서드는 서로 반환해주는 오브젝트가 동일하지 않다.
@Configuration public class DaoFactory { @Bean public UserDao userDao() { return new UserDao(connectionMaker()); } @Bean public ConnectionMaker connectionMaker() { return new MySQLConnection(); } } @Slf4j public class MainClass { DaoFactory daoFactory = new DaoFactory(); UserDao userDao1 = daoFactory.userDao(); UserDao userDao2 = daoFactory.userDao(); log.info("userDao: '{}'", userDao1); log.info("userDao: '{}'", userDao2); } }
출력 결과에서 알 수 있듯이, 두 개는 각기 다른 값을 가진 동일하지 않은 오브젝트이다. 즉 userDao를 매번 호출하여 UserDao 객체를 반환받을 경우 매 새로운 객체를 새롭게 생성하여 반환이 이루어진다. 반면 스프링의 애플리케이션 컨텍스트에 DaoFactory를 설정정보로 등록하고 getBean() 메서드를 이용하여 userDao라는 이름으로 등록된 오브젝트를 가져올 경우 매 호출마다 동일한 오브젝트가 호출되는 것을 볼 수 있다.
@Configuration public class DaoFactory { @Bean public UserDao userDao() { return new UserDao(connectionMaker()); } @Bean public ConnectionMaker connectionMaker() { return new MySQLConnection(); } } @Slf4j public class MainClass { public static void main(String[] args) throws SQLException, ClassNotFoundException { // DaoFactory DaoFactory daoFactory = new DaoFactory(); UserDao userDao1 = daoFactory.userDao(); UserDao userDao2 = daoFactory.userDao(); log.info("userDao: '{}'", userDao1); log.info("userDao: '{}'", userDao2); // ApplicationContext ApplicationContext applicationContext = new AnnotationConfigApplicationContext(DaoFactory.class); UserDao userDao3 = applicationContext.getBean("userDao", UserDao.class); UserDao userDao4 = applicationContext.getBean("userDao", UserDao.class); log.info("userDao: '{}'", userDao3); log.info("userDao: '{}'", userDao4); } }
애플리케이션 컨텍스트
DaoFactory랑 비슷한 방식으로 동작하는 IoC 컨테이너이다. 그러면서 동시에 이 애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 리지스트리이기도 하다. 스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다. (여기서의 싱글톤은 디자인 패턴과는 비슷한 개념이면서 다르다.)
서버 애플리케이션과 싱글톤
스프링은 주로 독립형 프로그램 보다는 엔터프라이즈 시스템을 위하여 제작되었기에 대부분 서버환경에서 사용된다.그렇기에 빈들을 싱글톤으로 제작한다.
스프링이 처음 설계됐던 대규모의 엔터프라이즈 서버환경은 서버 하나당 최대로 초당 수십에서 수백 번씩 브라우저나 여타 시스템으로 부터 요청을 받아 처리할 수 있는 높은 성능을 요구하는 환경이었으며, 또 하나의 요청을 처리하기 위해 데이터 액세스, 서비스, 비지니스, 프레젠테이션 로직 등의 다양한 기능을 담당하는 오브젝트들이 참여하는 계층형 구조로 이루어진 경우가 대부분이다. 매번 이런 요청으로 새로운 오브젝트가 생성되고 GC를 통해 제거되더라도 부하가 걸리면 서버가 감당하기 힘들어진다.
그래서 엔터프라이즈 분야에서는 서비스 오브젝트라는 개념을 일찍부터 사용해왔으며 서블릿은 자바 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다. 스펙에서 강제하진 않지만, 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들어두고, 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다.
이렇게 애플리케이션 안에서 제한된 수, 대게 하나의 오브젝트만 만들어서 사용하는 것이 싱글톤 패턴의 원칙이다. 따라서 서버환경에서는 서비스 싱글톤 사용이 강조된다. 하지만 사용하기 까다롭고 문제점이 존재하여 피해야 하는 패턴이라는 의미의 안티패턴이라는 이름으로 불리기도 한다.
싱글톤 패턴의 한계 및 단점
1. 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 생성한다.
- private 생성자를 가지고 있기에 상속을 할 수 없다.
2. 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
3. 스태틱 팩토리 메소드인 getInstance()를 만들고 이 메소드가 최초로 호출하는 시점에만 오브젝트를 제작하고 생성된 오브젝트는 스태틱 필드에 저장한다.
4. 한번 오브젝트가 만들어진 이후에는 getInstance()를 통해 제작된 스태틱 필드의 오브젝트를 넘겨준다.
5. 테스트를 하기 힘들다.
6. 서버환경에서는 싱글톤이 하나만 제작되는 것에 대한 보장을 하질 못한다.
7. 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못한다.
의존관계
자신이 의존하는 대상이 변경이 될 경우 자신도 변경이 일어나는 등 영향이 끼치는걸 의미한다. 위 코드에서 ConnectionMaker를 의존하는 UserDao 가 ConnectionMaker만 의존을 하기에 MySQLConnection이 변경이 되어도 코드가 변경이 되지 않는걸 확인할 수 있다. 이렇게 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경이 자유러워진다.
의존관계 주입방법
1. 클래스 모델이나 코드에서 런타임 시점 의존관계가 드러나지 않는다. (인터페이스에만 의존)
2. 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
3. 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어준다.
의존관계 주입은 위 3가지 조건을 충족하는 작업을 의미하며, 두 오브젝트의 관계를 맺도록 도와주는 제 3의 존재가 있다는 의미이다.
private ConnectionMaker connectionMaker; public UserDao() { // 설계 시점부터 MySQLConnection 이라는 구체적인 클래스의 존재를 알고있다. connectionMaker = new MySQLConnection(); }
UserDao의 의존관계 주입 기술을 다시 살펴보면 설계 시점부터 MySQLConnection이라는 클래스의 존재를 알고 있다. 따라서 모델링 때의 의존관계, 즉 ConnectiomMaker 인터페이스의 관계뿐 아니라 런타임 의존관계, 즉 MySQLConnectiom 오브젝트를 사용하겠다는 것 까지 UserDao가 결정하고 관리하고 있는 셈이다.
그래서 IoC 방식을 사용해서 UserDao로부터 런타임 의존관계를 드러내는 코드를 제거하고, 제 3의 존재에게 런타임 의존관계 결정 권한을 위임한다. 그래서 만들어진 것이 DaoFactory다. 이 DaoFactory는 의존관계 주입을 담당하는 컨테이너라고 볼 수 있으며, 줄여서 DI 컨테이너라고 불린다.
의존관계 주입코드
의존성 주입코드의 경우 아래와 같이 3가지가 존재한다. (Spring의 의존도를 떨어트리고, Spring을 사용하지 않는 단위테스트 등의 이유로 생성자 주입을 권장하는 편이다.)
private ConnectionMaker connectionMaker; // 생성자 주입 Spring 4.x 버전부터 @Autowired 생략이 가능해짐 public UserDao(ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; } // 필드 주입 @Autowired private ConnectionMaker connectiomMaker; // 세터 주입 Spring 3.x 버전까지 권장되어왔음 @Autowired public void setConnectiomMaker(ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; }
의존관계 검색과 주입
스프링이 제공하는 IoC 방법에는 의존관계 주입만 아니라 자신이 필요한 의존 오브젝트를 능동적으로 검색하여 찾는 의존관계 검색이라고 불리는 것도 있다.
의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메소드나 생성자를 통해 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.
// DaoFactory를 이용하는 생성자 public UserDao() { DaoFactory daoFactory = new DaoFactory(); this.connectionMaker = daoFactory.connectiomMaker(); } // 위 코드가 스프링에선 애플리케이션 컨텍스트에서 검색을 하는 역활이다. public UserDao() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class); this.connectiomMaker = context.getBean("connectionMaker", ConnectiomMaker.class); }
이 코드 역시 여전히 인터페이스만 의존하며 런타임 시에 DaoFactory가 만들어서 돌려주는 오브젝트와 런타임 의존관계를 맺는다. 따라서 IoC 개념을 잘 따르고 있으며, 혜택을 받는 코드이다. 하지만 적용방법이 외부의 주입이 아닌 DaoFactory에게 요청하는 작업이기에 이를 스프링의 애플리케이션 컨텍스트라면 미리 정해놓은 이름을 전달해서 해당하는 오브젝트를 찾게 하는 것이다. 따라서 이는 일종의 검색이라 불린다.
※DI(의존관계 주입) DL(의존관계 검색)
의존관계 검색은 코드 안에 불필요한 오브젝트 팩토리 클래스, 스프링 API가 드러나기에, 성격이 다른 오브젝트에 의존하게 되는 것이므로 바람직하지 않다. 추가로 DI의 경우 자신과 의존하는 대상(UserDao, ConnectiomMaker)둘다 Bean이어야 하지만 DL의 경우 의존하는 대상(ConnectiomMaker)만 Bean이어도 된다.
검색 보다는 주입이 편리하여 주입을 사용하지만 가끔 검색을 사용할 때가 존재한다.
1. 스태틱 메소드인 main()에서는 DI를 이용하여 오브젝트를 주입받을 방법이 없기에 검색을 사용
2. 서블릿에서 스프링 컨테이너에 담긴 오브젝트를 사용하려면 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야함
XML을 이용한 설정
스프링은 DaoFactory와 같은 자바 클래스를 이용하는 것 외에도 XML을 이용하여 DI를 설정할 수 있다. XML의 경우 별도 빌드 작업이 없으며, 환경이 달라져서 오브젝트의 관계가 바뀌는 경우에도 빠르게 변경사항을 반영할 수 있다.
XML 표
자바 코드 설정정보 XML 설정정보 빈 설정파일 @Configuration <beans> 빈의 이름 @Bean methodName() <bean id="methodName" 빈의 클래스 return new BeanClass(); class="a.b.c... BeanClass"> 이처럼 DI 설정을 할때 XML을 이용하여 설정을 하거나 DaoFactory와 같은 자바 클래스를 이용해서 할 수 있다.(Spring 3.x 버전 부터 사용이 가능하다.)
ref
그래서
-
DAO 코드를 점점 발전시키고(관심사 분리, 낮은 결합도 및 높은 응집도), 객체의 관리를 IoC컨테이너에 위임하고 DI(클래스 - 인터페이스간의 오브젝트 의존관계를 런타임시 구체적인 객체를 DI컨테이너의 도움으로 주입)등등 책을 읽고 재밋었다.
-
XML을 이용하여 DI 주입 보다는 요즘 유행은 이방법이니..., 가독성, xml파일 최소화(SpringBoot)등등의 이유로 JavaConfig를 선호한다고 하지만 알아서 케바케 or 개발환경에 맞게끔 하면 된다고 생각한다.
-
스프링을 사용하는 대표적인 목적인 DI(IoC)가 이해하기 힘들면서 확실히 재밋었다.
'Spring' 카테고리의 다른 글
토비의 스프링[3] 템플릿 (0) 2021.01.14 토비의 스프링[2] 테스트 (0) 2020.12.23 Spring Boot로 알아가는 Swagger (0) 2020.04.07 Spring JPA(5) 게시판 (0) 2020.02.18 Spring Boot HikariCP (0) 2020.02.04 -