일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch12
- 스프링 트랜잭션
- 스프링 mvc2 - 로그인 처리
- 자바 고급2편 - 네트워크 프로그램
- 스프링 mvc2 - 검증
- 스프링 mvc2 - 타임리프
- 2024 정보처리기사 수제비 실기
- 스프링 mvc1 - 스프링 mvc
- 데이터 접근 기술
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch14
- @Aspect
- 스프링 고급 - 스프링 aop
- 자바로 계산기 만들기
- 자바 고급2편 - io
- 자바 중급2편 - 컬렉션 프레임워크
- 자바로 키오스크 만들기
- 스프링 입문(무료)
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch1
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch11
- 자바 중급1편 - 날짜와 시간
- 람다
- 자바 기초
- 자바의 정석 기초편 ch4
- Today
- Total
개발공부기록
HTTP 서버 활용, HTTP 서버 - 애노테이션 서블릿(시작, 동적 바인딩, 성능 최적화), HTTP 서버 활용 - 회원 관리 서비스 본문
HTTP 서버 활용, HTTP 서버 - 애노테이션 서블릿(시작, 동적 바인딩, 성능 최적화), HTTP 서버 활용 - 회원 관리 서비스
소소한나구리 2025. 3. 7. 16:20출처 : 인프런 - 김영한의 실전 자바 - 고급2편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
HTTP 서버 - 애노테이션 서블릿
시작
애노테이션 기반의 컨트롤러와 서블릿 만들기
Mapping - 애노테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Mapping {
String value();
}
AnnotationServletV1
package was.httpserver.servlet.annotation;
public class AnnotationServletV1 implements HttpServlet {
private final List<Object> controllers;
public AnnotationServletV1(List<Object> controllers) {
this.controllers = controllers;
}
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
String path = request.getPath();
for (Object controller : controllers) {
Method[] methods = controller.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Mapping.class)) {
Mapping mapping = method.getAnnotation(Mapping.class);
String value = mapping.value();
if (value.equals(path)) {
invoke(controller, method, request, response);
return;
}
}
}
}
throw new PageNotFoundException("request= " + path);
}
private void invoke(Object controller, Method method, HttpRequest request, HttpResponse response) {
try {
method.invoke(controller, request, response);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
- 리플렉션에서 사용한 코드와 매우 유사함
- 차이가 있다면 호출할 메서드를 찾을 때 메서드의 이름을 비교하는 대신에 메서드에서 @Mapping 애노테이션을 찾고 그곳의 value 값으로 비교하는 차이가 있음
SiteControllerV7
package was.v7;
public class SiteControllerV7 {
@Mapping("/")
public void home(HttpRequest request, HttpResponse response) {
response.writeBody("<h1>home</h1>");
response.writeBody("<ul>");
response.writeBody("<li><a href='/site1'>site1</a></li>");
response.writeBody("<li><a href='/site2'>site2</a></li>");
response.writeBody("<li><a href='/search?q=hello'>검색</a></li>");
response.writeBody("</ul>");
}
@Mapping("/site1")
public void site1(HttpRequest request, HttpResponse response) {
response.writeBody("<h1>site1</h1>");
}
@Mapping("/site2")
public void site2(HttpRequest request, HttpResponse response) {
response.writeBody("<h1>site2</h1>");
}
}
- @Mapping("/"), home(): 애노테이션을 사용한 덕분에 / URL 요청도 컨트롤러에서 처리할 수 있게 되었음
- site1(), site2(): @Mapping 애노테이션을 활용하여 각 사이트의 URL을 매핑함
SiteControllerV7
package was.v7;
public class SearchControllerV7 {
@Mapping("/search")
public void search(HttpRequest request, HttpResponse response) {
String query = request.getParameter("q");
response.writeBody("<h1>Search</h1>");
response.writeBody("<ul>");
response.writeBody("<li>query: " + query + "</li>");
response.writeBody("</ul>");
}
}
- 기존에 만들었던 SearchController와 동일하며 search() 메서드에 @Mapping("/search")로 애노테이션과 메서드를 매핑함
ServerMainV7
package was.v7;
public class ServerMainV7 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
List<Object> controllers = List.of(new SiteControllerV7(), new SearchControllerV7());
HttpServlet servlet = new AnnotationServletV1(controllers);
ServletManager servletManager = new ServletManager();
servletManager.setDefaultServlet(servlet);
servletManager.add("/favicon.ico", new DiscardServlet());
HttpServer server = new HttpServer(PORT, servletManager);
server.start();
}
}
- 애노테이션을 활용한 AnnotationServletV1을 사용한 덕분에 ServerMainV6에서 / 경로에 별도의 HomeServlet을 등록하여 사용하던 코드가 제거되어 모든 경로의 URL을 Controller를 통해 관리할 수 있게 되었음
- /favicon.ico의 경우도 컨트롤러를 통해 해결해도 되지만 DiscardServlet이라는 공통 서블릿 기능이 있어서 유지함
- 서버를 실행하여 접속해 보면 기존과 동일하게 정상적으로 동작하는 것을 확인할 수 있음
정리
- 애노테이션을 사용하여 매우 편리하고 실용적으로 웹 애플리케이션을 만들 수 있게 되었음
- 현대의 웹 프레임워크들은 대부분 애노테이션을 사용하여 편리하게 호출 메서드를 찾을 수 있는 지금과 같은 방식을 제공하며 자바 백엔드의 사실상 표준 기술인 스프링 프레임워크도 스프링 MVC를 통해 이런 방식의 기능을 제공함
동적 바인딩
아쉬운 부분
직접 만든 애노테이션 기반 컨트롤러도 매우 잘 만들었지만 아쉬운 부분이 있다면 site1(), site2()의 경우 HttpRequest request가 전혀 필요하지 않고 HttpResponse response만 있으면 되지만 필요 여부와 관계없이 무조건 파라미터로 넘겨주어야 함
컨트롤러의 메서드를 만들 때 HttpRequest request, HttpResponse response 중에 딱 필요한 메서드만 유연하게 받을 수 있도록 AnnotationServletV1의 기능을 개선한 V2 버전을 작성
AnnotationServletV2
package was.httpserver.servlet.annotation;
public class AnnotationServletV2 implements HttpServlet {
// List, service() 코드 동일
private void invoke(Object controller, Method method, HttpRequest request, HttpResponse response) {
Class<?>[] parameterTypes = method.getParameterTypes();
// request, response 둘 다 받으면 length = 2
Object[] args = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
if (parameterTypes[i] == HttpRequest.class) {
args[i] = request;
} else if (parameterTypes[i] == HttpResponse.class) {
args[i] = response;
} else {
throw new IllegalArgumentException("Unsupported parameter type: " + parameterTypes[i]);
}
}
try {
method.invoke(controller, args);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
- invoke()
- getParameterTypes()를 통해 메서드의 매개변수의 타입을 배열로 반환함
- 이후 getParameterTypes()로 생성한 배열의 길이만큼 새로운 Object타입 배열을 생성
- 만약 @Mapping 애노테이션이 적용된 메서드의 매개변수에 request, response가 둘 다 사용되면 배열의 길이가 2가 되고 둘 중 하나만 사용되면 배열의 길이가 1이 됨
- 이후 반복문과 조건문을 통해서 HttpRequest를 사용하면 request를 담고, HttpResponse를 사용하면 response를 담고, 그 외의 상황을 예외를 던지도록 작성
- invoke()는 ...로 인자를 가변으로 넘길 수 있으므로 조건문에 따라 파라미터 타입이 담긴 Object[] args를 인자로 넘길 수 있음
SiteControllerV8, SearchControllerV8
package was.v8;
public class SiteControllerV8 {
@Mapping("/")
public void home(HttpResponse response) {
response.writeBody("<h1>home</h1>");
response.writeBody("<ul>");
response.writeBody("<li><a href='/site1'>site1</a></li>");
response.writeBody("<li><a href='/site2'>site2</a></li>");
response.writeBody("<li><a href='/search?q=hello'>검색</a></li>");
response.writeBody("</ul>");
}
@Mapping("/site1")
public void site1(HttpResponse response) {
response.writeBody("<h1>site1</h1>");
}
@Mapping("/site2")
public void site2(HttpResponse response) {
response.writeBody("<h1>site2</h1>");
}
}
- 이제 각 컨트롤러에서 실제 필요한 값만 매개변수로 선언하면 되기 때문에 SiteController에서 사용하지 않는 HttpRequest는 각 메서드의 매개변수에서 제거해도 됨
- SearchController는 둘 다 사용하므로 기존과 동일함
ServerMainV8
- SiteControllerV8, SearchControllerV8, AnnotationServletV2를 사용하도록 수정하고 실행 후 localhost:12345로 접속해 보면 기존과 동일하게 정상적으로 동작하는 것을 확인할 수 있음
정리
- AnnotationServletV2에서 호출할 컨트롤러 메서드의 매개변수를 먼저 확인한 다음에 매개변수에 필요한 값을 동적으로 만들어서 전달한 덕분에 컨트롤러의 메서드는 자신에게 필요한 값만 선언하고 전달받을 수 있게 되었음
- 이런 기능을 확장하면 HttpRequest, HttpResponse뿐만 아니라 다양한 객체들도 전달할 수 있음
- 참고로 스프링 MVC도 이런 방식으로 다양한 매개변수의 값을 동적으로 전달함
성능 최적화
AnnotationServletV2의 2가지 아쉬운 점
문제 1. 성능 최적화
- AnnotationServletV2의 service() 메서드의 구조를 보면 모든 컨트롤러의 메서드를 하나하나 순서대로 찾는데 이것은 결과적으로 O(n)의 성능을 보이게 됨
- 만약 모든 컨트롤러의 메서드가 100개라면 최악의 경우 100번을 찾아야 하며 더 큰 문제는 고객의 요청 때마다 이 로직이 호출되어 동시에 100명의 고객이 요청하면 100 * 100번 해당 로직이 호출되어 버림
- 이 부분의 성능을 O(n) -> O(1)로 개선해야 함
문제 2. 중복 매핑 문제
@Mapping("/site2")
public void site2(HttpResponse response) {
response.writeBody("<h1>site2</h1>");
}
@Mapping("/site2")
public void page2(HttpResponse response) {
response.writeBody("<h1>page2</h1>");
}
- 현재는 Controller에서 @Mapping() 애노테이션으로 URL을 매핑할 때 동일한 URL을 매핑해도 아무런 문제가 나타나지 않음
- 그러나 중복으로 매핑하게 되면 먼저 찾는 메서드 하나만 호출되어 site2(), page2 둘 중 하나만 동작하므로 중복 매핑이 된 다른 하나의 메서드는 호출되지 못함
- 그럼 누군가는 '어떤 경우에 무엇이 먼저 호출되는가'라고 질문할 수 있지만 이것은 접근 자체가 잘못되었는데, 이유는 개발에서 가장 나쁜 것이 모호한 것이기 때문임
- 모호한 문제는 제거하지 않으면 나중에 엄청난 장애가 발생할 수 있으므로 반드시 제거해야 함
AnnotationServletV3
package was.httpserver.servlet.annotation;
public class AnnotationServletV3 implements HttpServlet {
private final Map<String, ControllerMethod> pathMap;
public AnnotationServletV3(List<Object> controllers) {
this.pathMap = new HashMap<>();
initializePathMap(controllers);
}
private void initializePathMap(List<Object> controllers) {
for (Object controller : controllers) {
Method[] methods = controller.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Mapping.class)) {
String path = method.getAnnotation(Mapping.class).value();
if (pathMap.containsKey(path)) {
ControllerMethod controllerMethod = pathMap.get(path);
throw new IllegalStateException("경로 중복 등록, path= " + path + ", method= " + controllerMethod.method );
}
pathMap.put(path, new ControllerMethod(controller, method));
}
}
}
}
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
String path = request.getPath();
ControllerMethod controllerMethod = pathMap.get(path);
if (controllerMethod == null) {
throw new PageNotFoundException("request= " + path);
}
controllerMethod.invoke(request, response);
}
private static class ControllerMethod {
private final Object controller;
private final Method method;
public ControllerMethod(Object controller, Method method) {
this.controller = controller;
this.method = method;
}
public void invoke(HttpRequest request, HttpResponse response) {
Class<?>[] parameterTypes = method.getParameterTypes();
// request, response 둘 다 사용하면 length = 2
Object[] args = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
if (parameterTypes[i] == HttpRequest.class) {
args[i] = request;
} else if (parameterTypes[i] == HttpResponse.class) {
args[i] = response;
} else {
throw new IllegalArgumentException("Unsupported parameter type: " + parameterTypes[i]);
}
}
try {
method.invoke(controller, args);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
}
초기화
- AnnotaionServletV3을 생성하는 시점에 @Mapping을 사용하는 컨트롤러의 메서드를 모두 찾아서 pathMap에 보관
- 해당 Map의 key는 경로이고 value는 ControllerMethod임
- initializePathMap() 메서드를 보면 외부에서 등록된 컨트롤러들을 모두 순회하여 pathMap에 등록하고, 이때 중복이 발생하면 예외를 터트림
- 초기화가 끝나면 pathMap이 완성됨
- ControllerMethod: @Mapping의 대상 메서드와 메서드가 있는 컨트롤러 객체를 캡슐화를 진행
- ControllerMethod 객체를 사용하여 편리하게 실제 메서드를 호출할 수 있음
- method.invoke()를 실행하는 invoke() 메서드가 여기서 호출됨
실행
- ControllerMethod controllerMethod = pathMap.get(path)를 사용하여 URL 경로에 매핑된 컨트롤러의 메서드를 찾아옴
- 이 과정은 HashMap을 사용하므로 일반적으로 O(1)의 매우 빠른 성능을 제공함
ServerMainV8
- AnnotationServletV3을 사용하도록 코드를 실행해보면 기존과 동일하게 모두 정상적으로 실행 되는 것을 확인할 수 있음
- 중복 체크가 정상적으로 동작하는지 확인하기 위해 SiteControllerV8에 @Mapping("/site2")를 두 개 적용하여 실행해보면 예외가 발생하여 애플리케이션이 실행되지 않는 것을 확인할 수 있음

서버를 실행하는 시점에 바로 오류가 발생하고 서버 실행이 중단이 되어 서버 실행 시점에 발견할 수 있는 오류는 아주 좋은 오류임
만약 이런 오류를 체크하지 않고 /site2가 2개 유지된 대로 작동하면 고객은 기대한 화면과 다른 화면을 보고 있을 수 있음
오류는 3가지의 오류가 있음
- 컴파일 오류: 가장 좋은 오류, 프로그램 실행 전에 개발자가 가장 빠르게 문제를 확인할 수 있음
- 런타임 오류 - 시작 오류: 자바 프로그램이나 서버를 시작하는 시점에 발견할 수 있는 오류, 문제를 아주 빠르게 발견할 수 있기 때문에 좋은 오류이며 고객이 문제를 인지하기 전에 수정하고 해결할 수 있음
- 런타임 오류 - 작동 오류: 고객이 특정 기능을 실행할 때 발생하는 오류, 원인 파악과 문제 해결에 가장 많은 시간이 걸리고 가장 큰 피해를 주는 오류임
정리
AnnotationServletV3를 도입함으로써 성능, 유연성, 오류 체크 기능까지 강화된 애플리케이션이 되었음
HTTP 서버 활용 - 회원 관리 서비스
요구 사항
I/O에서 콘솔과 파일을 활용해 구현한 기능을 웹으로 다시 구현
회원의 속성은 아래와 같음
- ID
- Name
- Age
기존에 구현해 두었던 io.member의 패키지의 Member, MemberRepository는 그대로 재사용하여 구현
회원 컨트롤러
MemberController
package webservice;
public class MemberController {
private final MemberRepository memberRepository;
public MemberController(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Mapping("/")
public void home(HttpResponse response) {
String str = "<html><body>" +
"<h1>Member Manager</h1>" +
"<ul>" +
"<li><a href='/members'>Member List</a></li>" +
"<li><a href='/add-member-form'>Add New Member</a></li>" +
"</ul>" +
"</body></html>";
response.writeBody(str);
}
@Mapping("/members")
public void members(HttpResponse response) {
List<Member> members = memberRepository.findAll();
StringBuilder page = new StringBuilder();
page.append("<html><body>");
page.append("<h1>Member List</h1>");
page.append("<ul>");
for (Member member : members) {
page.append("<li>")
.append("ID: ").append(member.getId())
.append(", Name: ").append(member.getName())
.append(", Age: ").append(member.getAge())
.append("</li>");
}
page.append("</ul>");
page.append("<a href='/'>Back to Home</a>");
page.append("</body></html>");
response.writeBody(page.toString());
}
@Mapping("/add-member-form")
public void addMemberForm(HttpRequest request, HttpResponse response) {
String body = "<html><body>" +
"<h1>Add New Member</h1>" +
"<form method='POST' action='/add-member'>" +
"ID: <input type='text' name='id'><br>" +
"Name: <input type='text' name='name'><br>" +
"Age: <input type='text' name='age'><br>" +
"<input type='submit' value='Add'>" +
"</form>" +
"<a href='/'>Back to Home</a>" +
"</body></html>";
response.writeBody(body);
}
@Mapping("/add-member")
public void addMember(HttpRequest request, HttpResponse response) {
log("MemberController.addMember");
log("request = " + request);
String id = request.getParameter("id");
String name = request.getParameter("name");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(id, name, age);
memberRepository.add(member);
response.writeBody("<h1>save ok</h1>");
response.writeBody("<a href='/'>Back to Home</a>");
}
}
home()

- 첫 화면, 회원 목록과 신규 회원 등록 화면으로 이동하는 기능을 제공함
- Member List - /members
- Add New Member - /add-member-form
members()


- memberRepository.findAll() 기능을 사용해서 저장된 모든 회원 목록을 조회함
- 반복문을 사용하여 컬렉션에 담긴 회원 정보를 기반으로 HTML을 생성함
addMemberForm


- 회원을 저장하기 위해서는 회원을 등록하는 화면이 필요한데 HTML에서는 이것을 폼(form)이라 하며 이런 폼을 처리하기 위한 특별한 HTML 태그들을 지원함
- 출력된 화면에 값을 작성하고 Add 버튼을 누르면 HTTP 메시지가 전송됨
- <form> : 클라이언트에서 서버로 전송할 데이터를 입력하는 기능을 제공함
- method='POST': HTTP 메시지를 전송할 때 POST 방식으로 전송함, POST는 메시지 바디에 필요한 데이터를 추가해서 서버에 전달할 수 있음
- action='/add-member': HTTP 메시지를 전송할 URL 경로임
- <input type='text'>: 클라이언트에서 서버로 전송할 각각의 항목, name이 키로 사용됨
- <input type='submit'>: 폼에 입력한 내용을 서버에 전송할 때 사용하는 전송 버튼
클라이언트가 생성한 HTTP 요청 메시지
POST /add-member HTTP/1.1
host: localhost:12345
content-length: 22
content-type: application/x-www-form-urlencoded
id=3&name=name3&age=40
- Content-Length: 메시지 바디가 있는 경우 메시지 바디의 크기를 표현함
- Content-Type: 메시지 바디가 있는 경우 메시지 바디의 형태를 표현함
- application/x-www-form-urlencoded: HTML의 폼을 사용해서 전송한 경우로 input type에서 입력한 내용을 key=value 형식으로 메시지 바디에 담아서 전송함
- URL에서? 이후의 부분에 key1=value1&key2=value2 포맷으로 서버에 전송하는 것과 거의 같은 포맷으로 전송함
addMember()
- 메시지 바디에 담겨있는 id=3&name=name3&age=40 데이터를 꺼내서 회원 객체를 생성하고 MemberRepository를 통해서 회원을 저장소에 저장함
HttpRequest - 메시지 바디 파싱
HttpRequest - 추가
package was.httpserver;
public class HttpRequest {
private String method;
private String path;
private final Map<String, String> queryParameters = new HashMap<>();
private final Map<String, String> headers = new HashMap<>();
public HttpRequest(BufferedReader reader) throws IOException {
parseRequestLine(reader);
parseHeaders(reader);
// 메시지 바디 파싱 추가
parseBody(reader);
}
// 기존 메서드 동일 생략
// 메시지 바디 파싱 추가
private void parseBody(BufferedReader reader) throws IOException {
if (!headers.containsKey("Content-Length")) {
return;
}
int contentLength = Integer.parseInt(headers.get("Content-Length"));
char[] bodyChars = new char[contentLength];
int read = reader.read(bodyChars); // 버퍼의 데이터를 배열에 저장
if (read != contentLength) {
throw new IOException("Fail to read entire body. Expected " + contentLength + " bytes, but read " + read);
}
String body = new String(bodyChars);
log("HTTP Message Body: " + body);
String contentType = headers.get("Content-Type");
if ("application/x-www-form-urlencoded".equals(contentType)) {
parseQueryParameters(body);
}
}
}
parseBody()
- Content-Length가 있는 경우 메시지 바디가 있다고 가정하고 Content-Length의 길이만큼 스트림에서 메시지 바디의 데이터를 읽어옴
- 만약 읽어온 길이가 다르다면 문제가 있다고 예외를 던지고, 헤더에 Content-Length가 없으면 메시지 바디가 없으므로 그대로 return 함
- 이후 Content-Type을 체크하여 HTML 폼 전송인 application/x-www-form-urlencoded 타입이라면 URL의 쿼리 스트링과 같은 방식으로 파싱을 시도하고 파싱 결과를 URL의 쿼리 스트링과 같은 queryParameters에 보관함(Map 구조)
- 이런 방식으로 URL의 쿼리 스트링이든 HTML 폼 전송이든 getParameter()를 사용하여 같은 방식으로 데이터를 편리하게 조회할 수 있음
MemberServerMain
package webservice;
public class MemberServerMain {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
MemberRepository memberRepository = new FileMemberRepository();
MemberController memberController = new MemberController(memberRepository);
HttpServlet servlet = new AnnotationServletV3(List.of(memberController));
ServletManager servletManager = new ServletManager();
servletManager.setDefaultServlet(servlet);
servletManager.add("/favicon.ico", new DiscardServlet());
HttpServer server = new HttpServer(PORT, servletManager);
server.start();
}
}
- 앞서 작성한 AnnotationServletV3을 사용하여 서블릿들을 등록시키고 실행 후 localhost:12345로 접속해 보면 모든 동작이 정상적으로 실행되며 File에도 데이터가 정상적으로 저장되는 것을 확인할 수 있음
MemberRepository 인터페이스의 구현체
- I/O 강의에서 구현한 Memory, File, Data, Object 멤버 저장소 중 아무거나 사용해도 됨
- 여기에서는 저장한 데이터를 확인하기 쉽도록 FileMemberRepository를 사용하였음
new MemberController(memberRepository)
- MemberController는 MemberRepository가 필요한데 여기서 중요한 핵심은 MemberController는 MemberRepository 인터페이스에만 의존한다는 점으로 실제 런타임에 어떤 인스턴스가 들어오는지는 알 수 없음
- 런타임에 FileMemberRepository에서 ObjectMemberRepository로 변경하더라도 MemberController의 코드는 전혀 변경하지 않아도 됨
- MemberController 입장에서는 MemberRepository의 인스턴스를 외부에서 주입받는 것처럼 느껴진다고 하여 이것을 의존 관계 주입(Dependency Injection), 줄여서 DI라 함
정리
구성 역할, 사용 역할
- MemberServerMain 클래스의 역할은 본인이 어떤 코드 블록을 만든 것이 아니라 지금까지 있는 코드 블록들을 조립하는 일을 하여 어떤 MemberRepository를 사용할지, 어떤 컨트롤러를 사용할 지, 어떤 HttpServlet을 사용할 지 선택함
- 마치 레고 블록을 조립하는 것과 같이, 컴퓨터를 조립할 때 어떤 CPU를 사용하고 어떤 메모리, GPU를 선택할지 고르는 것처럼 필요한 컴포넌트를 구성(Configuration)하는 것 처럼 느껴짐
- 즉, MemberServerMain은 프로젝트를 구성하는 역할을 담당하고 나머지 클래스들은 실제 기능을 제공하는 사용 역할을 담당함
- 이렇게 구성하는 역할과 사용하는 역할을 명확하게 분리해 두면 다음과 같은 장점이 있음
- 유연성 향상: 프로젝트의 구성을 쉽게 변경할 수 있음, 예를 들어 다른 MemberRepository나 컨트롤러를 사용하고 싶을 때 MemberServerMain만 수정하면 됨
- 테스트 용이성: 각 컴포넌트를 독립적으로 테스트할 수 있음, 구성 로직과 실제 기능 로직이 분리되어 있어 단위 테스트가 더 쉬워짐
- 코드 재사용성 증가: 각 컴포넌트는 독립적이므로 다른 프로젝트에서도 쉽게 재사용할 수 있음
- 관심사의 분리: 구성 로직과 비즈니스 로직이 분리되어 각 부분에 집중할 수 있음
- 유지보수 용이성: 전체 시스템의 구조를 이해하기 쉬워지며 특정 부분을 수정할 때 다른 부분에 미치는 영향을 최소화할 수 있음
- 확장성 개선: 새로운 기능이나 컴포넌트를 추가할 때 기존 코드를 크게 수정하지 않고도 MemberServerMain에 새로운 구성을 추가할 수 있음
이러한 설계 방식을 효과적으로 구현하려면 소프트웨어 개발의 주요 원칙들을 준수해야 하는데 특히 다형성, 개방-폐쇄 원칙(OCP), 그리고 의존관계 주입(DI) 원칙이 중요함
이러한 접근법은 대규모 프로젝트에서 특히 유용함
하지만 구성과 사용의 역할을 명확히 구분하고 소프트웨어 개발의 핵심 원칙들을 적용하는 것은 쉽지 않은데, 이러한 과정을 크게 간소화해주는 도구가 바로 스프링 프레임워크임
스프링은 앞서 언급한 구성과 사용의 역할 분리, 그리고 소프트웨어 개발의 핵심 원칙들을 쉽고 효과적으로 적용할 수 있게 도와주는 실무 백엔드 개발의 핵심 기술임