기존의 프런트 컨트롤러에서 생성하던 페이지 컨트롤러와 DAO 객체의 생성을 ApplicationContext로 위임하여 프로그램을 실행시키면 객체가 자동으로 생성되게끔 프로그래밍하였다. 해당 과정에 대한 자세한 내용은 아래 피드에 기록해 놓았다.
https://ohreallystore.tistory.com/35
220322 (5) : 객체 생성 자동화
개요 ContextLoaderListener.java @WebListener public class ContextLoaderListener implements ServletContextListener { private static final Logger LOG = Logger.getGlobal(); @Override public void contex..
ohreallystore.tistory.com
해당 포스팅에서는 객체 생성 자동화에 대한 이유로 '페이지 컨트롤러에서 객체를 생성할 경우 비효율적이다'라는 이유를 들었다. 이번 포스팅에서는 왜 컨트롤러에서 객체를 '직접' 생성하는 것이 비효율적이고, 문제가 되는지에 대해 자세히 이야기해 보겠다.
무엇이 문제인가?
if (servletPath.equals("/auth/login.do")) {
pageController = new LogInController();
} else if (servletPath.equals("/feed/main.do")) {
pageController = new FeedListController().setFeedDao(feedDao);
} else if (servletPath.equals("/feed/content.do")) {
...
} else if (servletPath.equals("/feed/add.do")) {
...
} else if (servletPath.equals("/feed/edit.do")) {
...
} else if (servletPath.equals("/feed/delete.do")) {
...
} else {
pageController = null;
}
자동화하기전 프런트 컨트롤러(DispatcherServlet)에서 페이지 컨트롤러를 생성하던 코드다. 클라이언트로 부터 전달받은 URL을 확인하여 생성할 페이지 컨트롤러 구현체를 선정한다.
이 코드가 무엇이 문제일까? 결론부터 이야기하자면 이 코드는 객체 지향 프로그래밍의 원칙을 위배하는 코드이다. 지금부터 어떤 부분에서 객체지향 원칙을 어기는지 알아보자.
하나의 객체는 하나의 책임만을 가져야 한다. (SRP 원칙)
프런트 컨트롤러는 주어진 자원을 이용하여 작업을 수행하는 '사용역역'에 속하는 객체이다. 실제 프런트 컨트롤러는 주어진 페이지 컨트롤러를 실행시키고 리턴 받은 뷰의 경로를 인클루딩(또는 리다이렉팅)하는 역할을 수행한다.
하지만 위에 코드에서는 그런 작업뿐만 아니라 페이지 컨트롤러를 생성하는 역할까지 도맡아 하고 있다. 즉, 프런트 컨트롤러에 너무 많은 역할이 과중된것이다.
if (servletPath.equals("/auth/login.do")) {
pageController = new LogInController();
}
페이지 컨트롤러 구현체의 생성자를 통해 직접 구현체를 생성하고 있다.
좋은 객체 지향 프로그래밍은 하나의 객체의 하나의 책임만이 주어져야 한다. 즉 작업을 수행하는 코드(사용영역)가 작업에 필요한 자원까지 생성하는 것(구성 영역)은 이러한 원칙을 위배하는 것이다.
확장에는 열려있고, 수정에는 닫혀있어야 한다.(OCP 원칙)
많은 웹 서비스들은 확장되기 마련이다. 새로운 페이지가 생길수도 있고, 기존에 있던 페이지가 바뀔 수도 있다. 예를 들어, 새로운 페이지가 생겨 페이지 컨트롤러가 추가되었다고 가정해보자. 그러면 위에 코드에서는 새로운 else-if 문을 추가하여 해당 페이지 컨트롤러를 생성하도록 하여야 할 것이다.
if (servletPath.equals("/auth/login.do")) {
pageController = new LogInController();
} else if (servletPath.equals("/feed/main.do")) {
pageController = new FeedListController().setFeedDao(feedDao);
} else if (servletPath.equals("/feed/content.do")) {
...
} else if (servletPath.equals("/feed/add.do")) {
...
} else if (servletPath.equals("/feed/edit.do")) {
...
} else if (servletPath.equals("/feed/delete.do")) {
...
} else if (servletPath.equals("/new_path.do")) {
pageController = new NewPage();
} else {
pageController = null;
}
NewPage라는 새로운 페이지가 생겼다면 위와 같이 프런트 컨트롤러가 수정된다.
새로운 페이지가 생기거나, 페이지의 변경사항이 발생하는 것은 매우 흔한 일이다. 문제는 이러한 일이 일어날때마다 프런트 컨트롤러는 수정을 일으킨다. 이는 수정에는 닫혀있어야 한다는 OCP원칙을 위배한다.
확장에 열려있고, 수정에 닫혀있다는 말이 사실 와닿기 어렵다. 확장은 하지만 수정은 하지 말라니... 이를 쉽게 풀어쓰면 변경이 잦은 구현체 등이 변경될때 마다 다른 객체들이 연쇄적으로 수정 되어서는 안된다는 뜻이다. 이를 위해서는 각각의 객체가 구현체가 아닌 인터페이스(추상화)에만 의존할 필요성이 있다. 이는 바로 다음에 설명할 DIP(의존관계 역전의 원리)와 이어진다.
추상화에 의존해야지, 구체화에 의존하면 안된다. (DIP 원칙)
기존 코드는 if-else 문을 통해 페이지 컨트롤러 인터페이스의 구현체를 직접 생성하게 된다. 프런트 컨트롤러가 구현체를 직접 생성하면 당연히 각각의 구현체와 의존관계가 형성된다. 즉, 객체가 추상화(인터페이스)가 아니라 구체화(구현체)에 의존하게 된다. 이는 구체화의 의존하지 말라는 DIP 원칙을 위배하게 된다.
왜 구체화에 의존하면 안될까? 구체화에 해당하는 구현체는 변경이 잦다. 기술이 변경되거나, 정책이 바뀔때마다 구현체는 변경된다. 반대로 추상화에 해당하는 인터페이스는 변경이 매우 적다. 즉, 구현체에 의존하게 되면, 서비스가 변경/확장 될때 마다 객체가 수정되게 되고, 이는 유지/보수의 어려움을 일으킨다.
=> DIP 원칙의 위배는 OCP원칙에 위배를 일으킨다.
관심사 분리 : 사용영역과 구성영역을 분리하자.
기존의 코드가 객체지향의 원칙들을 위배한 가장 큰 원인은 객체 스스로가 사용할 자원들을 스스로 생성하기 때문이다. 즉, 생성과 사용이라는 두가지 역할을 하나의 객체가 도맡아 하기 때문이다. 이를 해결하기 위해서는 사용영역과 구성영역을 분리할 필요가 있다. 이를 관심사 분리라고 한다.
프런트 컨트롤러는 사용영역에 해당하는 객체로 기존의 페이지 컨트롤러를 생성하는 파트를 구성영역으로 분리를 해야 한다. 이를 위해 다양한 방법을 사용할 수 있는데 그러한 방법들을 알아보자.
Reflection API와 ContextLoaderListener
드디어 오늘의 주제로 왔다. 위에 나열한 수많은 문제를 해결하는 것이 객체 생성 자동화이다. 이를 순수한 자바 코드로 구현하기 위해서는 크게 2가지 도구가 필요한데 ContextLoaderListener와 ReflectionAPI이다.
ContextLoaderListener는 서블릿 프로그램에 생명주기를 관리하는 클래스로 프로그램이 실행 직후 실행할 코드들을 정의할 수 있다.
Reflection API는 클래스의 정보(이름,필드,메서드)등을 클래스에 대한 자세한 정보 없이 가져올 수 있는 API이다. 해당 API를 통해 필요한 객체를 미리 생성하고, 세팅할 수 있다.
자세한 내용은 아래 피드를 확인하길 바란다.
https://ohreallystore.tistory.com/35
220322 (5) : 객체 생성 자동화
개요 ContextLoaderListener.java @WebListener public class ContextLoaderListener implements ServletContextListener { private static final Logger LOG = Logger.getGlobal(); @Override public void contex..
ohreallystore.tistory.com
스프링 컨테이너
스프링 컨테이너는 스프링 프레임워크가 사용하는 객체로 스프링 빈이라는 개념을 도입하여 프로그램에서 사용되는 객체들을 스프링 빈으로 미리 등록하고, 실제 객체들을 생성(구성)없이 실제 사용만 하여 구성영역과 사용영역을 분리하는 기술이다. 자세한 내용은 이후 포스팅하겠다.
마무리
객체 지향의 다양한 원칙을 예로 들며 프런트 컨트롤러가 구현체를 직접 생성하는 것이 무엇이 문제이고, 객체 생성을 자동화하여 해당 문제를 어떻게 해결할 수 있는지 설명하였다. 요약하자면, 구현체를 직접 생성하면 하나 하나의 구현체의 모드 의존하게 된다. 변경이 잦은 구현체와의 의존관계는 객체의 잦은 수정을 불러일으키고, 이는 곧 유지보수의 어려움으로 이어진다.
이를 해결하기 위해서는 구현체의 생성과 실제 사용을 분리할 필요가 있다. 즉, 사용영역과 구성영역을 분리하는 것이다. 이를 위해 객체 생성을 자동화하여 미리 생성할 필요가 있다. Reflection API와 ContextLoaderListener를 통해 순수한 자바 코드로 객체 생성을 자동화할 수도 있고, 스프링 프레임워크를 적용하여 스프링 빈으로 등록하는 것도 하나의 방법이다.
'프로젝트 이야기 > CRUD 미니 게시판' 카테고리의 다른 글
220511 (8) : AWS 서버에 배포하기 (0) | 2022.05.11 |
---|---|
220413 (7) : DAO / Service 분리 (0) | 2022.04.13 |
220327 (6) : 댓글 기능 구현 (0) | 2022.03.27 |
220323 [bug] : 줄바뀜이 읽기/수정 폼에서 적용이 안되는 문제 (0) | 2022.03.23 |
220322 (5) : 객체 생성 자동화 (0) | 2022.03.22 |