이 글은 내 코드가 그렇게 이상한가요? 책을 읽고 정리한 내용을 바탕으로 작성하였습니다.
프로그래밍이나 컴퓨터 용어를 기반으로 이름 붙이는 것을 기술 중심 명명
이라 부른다.
그리고 클래스와 메서드에 번호를 붙여서 이름을 짓는 것을 일련번호 명명
이라 부른다.
이렇게 이름을 지으면 코드를 읽고 이해하는데 시간이 오래걸리고, 충분히 이해하지 못한 코드를 변경하면 버그가 발생하게 됩니다.
따라서 의도와 목적을 드러내는 이름을 사용하는 것이 좋다.
if문의 중첩이 많을 수록 코드의 가독성이 나빠진다.
어디서부터 어디까지가 if 조건문 처리문인지 확인하기 힘들기 때문이다.
데이터 클래스
는 설계가 제대로 이루어지지지 않는 소프트웨어에서 빈번하게 등장하는 클래스 구조이다.
금액을 다루는 서비스를 예로 들어 데이터 클래스의 어떤 점이 나쁜지 살펴보자.
데이터밖에 없는 클래스 구조
// 계약 금액
public class ContractAmount {
public int amountIncludingTax; // 세금 포함 금액
public BigDecimal salesTaxRate; // 소비세율
}
세금이 포함된 금액과 소비세율을 public
인스턴스 변수로 갖고 있으므로, 클래스 밖에서도 데이터를 자유롭게 변경할 수 있는 구조이다.
이처럼 데이터를 갖고 있기만 하는 클래스를 데이터 클래스
라고 부른다.
그런데 데이터 클래스에는 데이터뿐만 아니라, 세금이 포함된 금액을 계산하는 로직도 필요한데,
이러한 계산 로직을 데이터 클래스가 아닌 다른 클래스에 구현하는 일이 벌어지곤 한다.
이럴 경우, 작은 규모의 애플리케이션이라면 큰 문제가 되진 않지만, 규모가 큰 애플리케이션이라면 수많은 악마를 불러들인다.
예를 들어, 업무 계약 서비스에서 소비세와 관련된 사양이 변경되었다고 했을 때, 구현 담당자는 소비세율과 관련된 로직을 변경했다.
이런 상황은 데이터를 담고 있는 클래스와 데이터를 사용하는 계산 로직이 멀리 떨어져 있을 때 자주 일어난다.
이처럼 데이터와 로직 등이 분산되어 있는 것을 응집도가 낮은 구조라고 한다.
응집도가 낮아 생길 수 있는 여러 가지 문제는 아래와 같다.
[1] 코드 중복
관련된 코드가 서로 멀리 떨어져 있으면, 관련된 것끼리 묶어서 파악하기 힘들다.
이미 기능이 구현되어 있는데도, 해당 코드를 확인하지 못해서 같은 로직을 여러 곳에 구현할 수 있다.
정리하면, 의도하지 않게 코드 중복이 발생하는 것이다.
[2] 수정 누락
코드 중복이 많으면, 요구사항이 변경될 때 중복된 코드를 모두 고쳐야 한다.
하지만 이 과정에서 일부 코드를 놓칠 수 있으며, 결국 버그를 낳게 된다.
[3] 가독성 저하
가독성
이란 코드의 의도나 처리 흐름을 얼마나 빠르게 정확하게 읽고 이해할 수 있는지 나타내는 지표다.
코드가 분산되어 있으면, 찾기도 그 만큼 오래 걸린다.
[4] 초기화되지 않는 상태(쓰레기 객체)
초기화해야 하는 클래스라는 것을 모르면, 버그가 발생하기 쉬운 불완전한 클래스가 된다.
이처럼 초기화되지 않으면 쓸모 없는 클래스
또는 초기화하지 않는 상태가 발생할 수 있는 클래스
를 안티 패턴 쓰레기 객체라고 부른다.
[5] 잘못된 값 할당
값이 잘못되었다는 것은 요구 사항에 맞지 않음을 의미한다.
예를 들면) 주문 건수가 음수가 나오는 경우이다.
결과적으로 이와 같은 문제들은 개발 생산성을 떨어뜨리게 된다.
이 글은 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 바탕으로 정리한 내용입니다.
Spring MVC에 대해서는 예전 글에서 배경, 장점, 한계에 대해 어느 정도 정리를 했다.
이번 글은 HTTP 요청 처리가 왔을 때 Spring MVC 구조
가 어떻게 동작하는지 과정을 정리하는 글이다.
동작에 대해 대해 설명하기 전에 MVC에 대해 간단하게 정리하고 넘어가보자.
MVC는 하나의 서블릿이나, JSP로 처리하는 것을 Controller와 View라는 영역으로 서로 역할을 나눈 것을 말한다.
Controller
: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다.
그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
Model
: 뷰에 출력할 데이터를 담아둔다.
뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
View
: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다.
여기서는 HTML을 생성하는 부분을 말한다.
컨트롤러(Controller)에 비즈니스 로직을 둘 수도 있지만, 그렇게 되면 컨트롤러에 너무 많은 역할을 담당한다.
그래서 일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도로 만들어서 처리한다.
그리고 컨트롤러(Controller)는 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당한다.
위 그림에서 디스패처 서블릿(DispatcherServlet)
이 Spring MVC의 핵심이다.
DispacherServlet 서블릿 등록
DispacherServlet
도 부모 클래스에서 HttpServlet
을 상속 받아서 사용하고, 서블릿으로 동작한다.
스프링 부트는 DispacherServlet
을 서블릿으로 자동으로 등록하면서 모든 경로(urlPatterns="/")
에 대해서 매핑한다.
서블릿이 호출되면 HttpServlet
이 제공하는 serivce()
가 호출된다.
DispatcherServlet
의 부모인 FrameworkServlet
에서 service()
를 **오버라이드 **해두었다.FrameworkServlet 클래스 -
service()
FrameworkServlet.service()
를 시작으로 여러 메서드가 호출되면서 DispacherServlet.doDispatch()
가 호출된다.
DispacherServlet
클래스는 1500줄 가까이 되기 때문에요청 흐름
에 필요한 부분만 가져왔다.
public class DispatcherServlet extends FrameworkServlet {
@SuppressWarnings("deprecation")
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 2. 핸들러 어댑터 조회-핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// processDispatchResult -> 내부 로직
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
// 밑 부분은 안봐도 됨
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
}
public class DispatcherServlet extends FrameworkServlet {
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
// 뷰 랜더링 호출
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
}
}
public class DispatcherServlet extends FrameworkServlet {
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
// 6. 뷰 리졸버를 통해서 뷰 찾기
// 7.View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);
}
}
Spring MVC 구조를 보면서 동작 순서에 대해 다시 한번 확인해보자.
핸들러 어댑터 실행: 핸들러 어댑터를 실행한다.
핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행한다.
ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
viewResolver 호출: 뷰 리졸버를 찾고 실행한다.
InternalResourceViewResolver
가 자동 등록되고, 사용된다.View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고,렌더링역할을 담당하는뷰객체를 반환한다.
InternalResourceView(JstlView)
를 반환하는데, 내부에 forward()
로직이 있다.Spring MVC의 큰 강점은 DispatcherServlet
코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다는 점이다.
지금까지 설명한 대부분을 확장 가능할 수 있게 인터페이스로 제공한다.
이 인터페이스들만 구현해서 DispatcherServlet
에 등록하면 나만의 컨트롤러를 만들 수도 있다.
주요 인터페이스 목록
핸들러 매핑: org.springframework.web.servlet.HandlerMapping
핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter
뷰 리졸버: org.springframework.web.servlet.ViewResolver
뷰: org.springframework.web.servlet.View
Spring MVC는 코드 분량도 매우 많고, 복잡해서 내부 구조를 다 파악하기 쉽지 않다.
MVC는 이미 전세계 수 많은 개발자들의 요구사항에 맞추어 기능을 계속 확장했기 때문에 나만의 컨트롤러를 만드는 일은 없다.
그래서 애플리케이션을 만들 때 필요로 하는 대부분의 기능이 이미 다 구현되어 있다.
그래도 이렇게 핵심 동작방식을 알아두면 향후 문제가 발생했을 때 어떤 부분에서 문제가 발생했는지 쉽게 파악하고, 문제를 해결할 수 있다.
우선 전체적인 구조가 이렇게 되어있구나 하고 이해하자.
HTTP 주요 특징은 무상태성과, 비연결성이다.
무상태성
은 서버가 클라이언트의 상태를 보존하지 않는다는 것을 의미하고, 비연결성
은 말 그대로 연결을 유지하지 않는다는 것을 의미한다.
두가지 특징에 대한 자세한 설명은 HTTP 기본에 작성해두었다.
예를 들어, 홈페이지에서 로그인을 하고 난 뒤에 새로 고침을 하게 되면 로그인이 풀리는 상태가 된다.
로그인을 유지하기 위해서 서버가 다수의 클라이언트와 연결을 유지할 수 있지만, 그로 인해 자원이 낭비가 된다.
이러한 HTTP의 2가지 특징을 보완하기 위해 쿠키
가 등장하게 되었다.
쿠키
(Cookie)는 사용자(클라이언트)가 어떤 웹 사이트를 방문할 때, 사용자의 웹 브라우저를 통해 사용자 로컬에 키(Key)와 값(Value) 을 저장하는 작은 데이터 파일이다.서버는 클라이언트의 로그인 요청에 대한 응답을 작성할 때, 클라이언트 측에 저장하고 싶은 정보를 응답 헤더의 Set-Cookie
에 담아 전달한다.
이후 해당 클라이언트는 요청을 보낼 때마다 요청 헤더에 Cookie
를 담아 전송한다.
쿠키에 담긴 정보를 통해 서버는 해당 요청의 클라이언트가 누구인지 식별한다.
쿠키는 세션 쿠키
와 지속 쿠키
, 두 가지 타입으로 나뉜다.
세션 쿠키
(Session Cookie) : 브라우저 메모리에 저장되므로, 사용자가 사이트를 검색할 때 관련된 설정과 선호 사항들을 저장하는 임시 쿠키로 브라우저를 종료하면 해당정보가 삭제된다.
지속 쿠키
(Persistent Cookie) : 파일로 저장되므로, 브라우저가 종료되거나 컴퓨터가 재시작되어도 해당정보가 남아있다.
두 가지 쿠키를 구분하는 기준은 파기되는 시점이다. 파기되는 시점을 가리키는 Expires
혹은 Max-Age
파라미터가 없으면 세션 쿠키이다.
Set-Cookie: <쿠키 이름>=<쿠키 값>; Expires=종료 시점
Set-Cookie: <쿠키 이름>=<쿠키 값>; Max-Age=유효 기간
쿠키가 브라우저, 즉 클라이언트 측에 저장된다라는 것은 상당히 널리 알려진 사실이다. 이 부분은 쿠키가 탄생하게 된 중요한 배경이며, 안타깝게도 갖가지 이슈가 존재한다.
따라서 쿠키를 사용할 때는 아래와 같은 쿠키의 한계점들을 잘 인지하고 사용하는 것이 중요하다.
쿠키의 값이 브라우저에서 확인할 수 있어서 누군가로부터 유출 및 조작 당할 위험이 존재하므로 보안에 취약하다.
쿠키는 작은 데이터 파일로, 용량에 제한이 있기 때문에 많은 정보를 담을 수 없다.
웹 브라우저마다 쿠기에 대한 지원 형식이 다르기 때문에 브라우저 간에 공유가 불가능하다.
쿠기의 사이즈가 커질수록 네트워크에 부하가 심해진다.
이러한 쿠키의 한계와 대체 기술에도 불구하고 반드시 쿠키를 사용해야 하는 상황이라면 가급적 보안 속성을 사용하시기를 권장한다.
Secure
이다. Set-Cookie
응답 헤더에 이 속성이 명시된 쿠키는 브라우저가 https
프로토콜 상에서만 서버로 돌려 보낸다. 네트워크 상에서 탈취되었을 때 문제가 될 수 있는 쿠키를 상대로 쓰면 유용할 것이다.Set-Cookie: <쿠키 이름>=<쿠키 값>; Secure
HttpOnly
이다.
Set-Cookie
응답 헤더에 이 속성이 명시된 쿠키는 이 속성이 명시된 쿠키는 브라우저에서 자바스크립트로 Document.cookie
객체를 통해 접근할 수 없다.Set-Cookie: <쿠키 이름>=<쿠키 값>; HttpOnly
쿠키는 브라우저에 저장하기 때문에 보안에 취약하는 단점이 있다.
쿠키의 가장 큰 단점인 유출 및 조작 당할 위험을 보완하기 위해 세션
을 사용한다.
세션은 비밀번호와 같은 클라이언트의 인증 정보를 쿠키가 아닌 서버에 저장하고 관리한다.
클라이언트가 서버측에 요청을 보내면, 해당 서버의 엔진이 클라이언트에게 유일한 ID를 부여하는데, 이때 이 유일한 ID가 세션ID
이다.
세션ID
을 생성하여 서버의 메모리에 저장한다.세션ID
를 쿠키에 담아서 전달한다.세션ID
를 전달한다.세션ID
의 유효성을 판별하여 클라이언트를 식별한다.장점1 : 쿠키를 포함한 요청이 외부에 노출되더라도 세션ID 자체는 유의미한 개인정보를 담고 있지 않아서 비교적 안전하다.
단점1 : 하지만 누군가가 중간에 세션ID를 탈취해서 클라이언트인척 위장할 수 있다는 한계는 존재한다.
장점2 : 각 사용자마다 유일한 ID인 세션ID가 발급되므로, 요청이 들어올 때마다 회원 정보를 확인할 필요가 없다.
단점2 : 클라이언트의 인증 정보가 서버에 메모리에 저장되므로 클라이언트의 요청이 많아지면 서버에 부하가 심해진다.
쿠키 | 세션 | |
---|---|---|
저장위치 | 쿠키는 브라우저에 메모리 또는 파일로 저장된다 | 세션은 서버의 메모리에 저장된다 |
보안 | 쿠키는 브라우저에서 확인할 수 있으므로 유출 및 조작 당할 위험이 존재한다 | 세션은 클라이언트 정보가 서버의 메모리에 저장되므로 비교적 안전하다 |
라이프 사이클 | 쿠키는 파일로 저장될 때 브라우저가 종료되어도 정보가 남아있다 | 세션은 서버의 만료시간/날짜가 지나면 사라지거나 브라우저 종료시 세션ID가 사라진다 |
속도 | 쿠키에 정보가 있기 때문에 서버에 요청시 속도가 빠르다 | 세션은 서버의 메모리로부터 세션ID를 조회해야 하므로 속도가 쿠키보다 비교적 느리다 |
세션을 주로 사용하면 좋은데, 왜 굳이 쿠키를 사용하는 이유는?
세션은 브라우저가 아닌 서버에 데이터를 저장하므로, 서버의 메모리를 계속 사용하면 속도 저하가 올 수 있기 때문이다.
웹 개발에서 쿠키를 사용할 수 밖에 없는 결정적인 이유
HTTP 프로토콜은 서버가 클라이언트의 상태를 보존하지 않는 무상태성과 연결을 유지시키지 않는 비연결성 특징을 가지고 있다.
즉, 서버가 클라이언트의 요청에 응답을 하는 순간 HTTP 연결은 끊어지며, 클라이언트에서 새로운 요청을 해야 다시 HTTP 연결이 맺어지게 된다.
간단한 웹사이트가 아닌 이상 대부분의 서비스에서는 하나의 브라우저로 부터 순차적으로 들어오는 여러 개의 요청이 동일한 사용자로 부터 오는 것이라는 것을 알아야 한다.
클라이언트와 연결이 유지되지 않는 상황에서 동시에 서버로 유입되는 수많은 요청이 각각 어느 사용자의 것인지 판단하는 것은 서버 입장에서 매우 힘든 일이다.
여기서 쿠키의 지속성이 빛을 발휘하게 된다.
바로 서버가 쿠키를 한 번 브라우저에 저장하면 브라우저는 해당 쿠키를 매 요청마다 계속해서 서버로 돌려 보낸다는 것이다. 다시 말해 서버가 브라우저에 쿠키 하나만 심어 놓으면 그 후로 브라우저는 성실하게 매번 서버를 방문할 때 마다 해당 쿠키를 다시 가져온다.
이러한 쿠키의 특성을 활용하면 서버는 각 요청이 어느 브라우저에서 오는 것인지 어렵지 않게 판단할 수 있다.
예를 들어, 사용자가 서비스에 최초로 접속했을 때 서버가 브라우저에게 a=1
쿠키를 저장하라고 시키면,
HTTP 요청
GET /index.html HTTP/1.1
Host: www.test.com
HTTP 응답
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: a=1
해당 브라우저는 사용자가 www.test.com
이라는 도메인에 머무는 한 /index.html
을 방문하든 /about.html
을 방문하든 /contact.html
을 방문하든 매번 같은 쿠키를 돌려준다.
그러므로 서버 입장에서는 a=1
쿠키를 들고 들어오는 요청은 모두 이 브라우저로 부터 오는 것이구나라고 쉽게 알 수 있다.
(물론 다른 브라우저에게 a=1
대신에 a=2
, a=3
와 같은 다른 쿠키를 응답해줘야 한다)
쿠키는 사용자를 식별하고 세션을 유지하는 방식 중에서 현재까지 널리 사용하는 방식이다.
쿠키는 캐시와 충돌할 우려가 있으므로 대부분의 캐시나 브라우저는 쿠키에 있는 정보를 캐싱하지 않는다.
쿠키 | 캐시 | |
---|---|---|
정의 | 쿠키는 정보를 저장하기 위해 사용된다. 기본적으로 웹서버에서 PC로 보내는 작은 파일들을 저장한다. 보통 쿠키는 누군가 특정한 웹 사이트를 접속할 때 발생한다. | 캐시 또한 웹 페이지 요소를 저장하기 위한 임시 저장소이다. 특히, 나중에 필요할 것 같은 요소들을 저장한다.이러한 요소들은 그림 파일이나 문서 파일 등이 될 수 있다. |
목적 | 쿠키는 사용자의 인증을 도와준다. | 캐시는 웹 페이지가 빠르게 렌더링 할 수 있도록 도와준다. |
삭제 | 쿠키는 만료기간이 있어 시간이 지나면 자동삭제 된다. | 캐시는 사용자가 직접 수동으로 삭제해주어야한다. |
예시 | 유저의 선호도(로그인 정보, 방문기록, 방문횟수) | 오디오, 비디오 파일 |