1. Spring Dependencies

1 Java 1.8
2 lombok @기반 코드 자동 완성해주는 라이브러리
3 swagger 개발한 REST API 편리하게 문서화 해주는 오픈소스 프레임워크
4 jpa 개발자가 SQL 쿼리 없이 데이터베이스와 상호작용할 수 있게 해주는 API
5 mysql 오픈소스 관계형 DB
6 thumbnailator 썸네일 이미지 만드는 라이브러리
7 log4j 로그 라이브러리

2. gradle 설정

import java.time.LocalDate

buildscript {
	ext {
		queryDslVersion = "5.0.0"
	}
}

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.18'
	id 'io.spring.dependency-management' version '1.1.4'
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

version = '0.0.1-SNAPSHOT'

bootJar {
	mainClass = 'com.test.Application'
	archivesBaseName = 'test'

	def dateFormatted = LocalDate.now().toString().replace("-", "")
	archiveFileName = "test-${project.version}_${dateFormatted}.jar"
	duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

group = 'test'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(8)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// 기본 Spring Boot 의존성
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	// QueryDSL 관련 의존성
	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
	annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
	annotationProcessor 'javax.annotation:javax.annotation-api:1.3.2'
	annotationProcessor 'javax.persistence:javax.persistence-api:2.2'

	// 기타 의존성
	implementation 'mysql:mysql-connector-java:8.0.32'
	implementation 'com.h2database:h2'
	implementation 'net.coobird:thumbnailator:0.4.14'
	implementation 'org.springframework.boot:spring-boot-starter-log4j2'
	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
	implementation 'org.springdoc:springdoc-openapi-ui:1.6.12' // swagger

	// Lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	// 테스트 의존성
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

	// Testcontainers
	testImplementation 'org.testcontainers:testcontainers:1.17.6'
}

//querydsl 추가 시작
def querydslDir = "src/main/generated"

querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}
sourceSets {
	main.java.srcDir querydslDir
}
compileQuerydsl{
	options.annotationProcessorPath = configurations.querydsl
}
configurations {
	all {
		exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
	}
	compileOnly {
		extendsFrom annotationProcessor
	}
	querydsl.extendsFrom compileClasspath
}
//querydsl 추가 끝

 

'Java > Spring' 카테고리의 다른 글

[Spring] MVC 발전과정(2)  (0) 2024.10.31
[Spring] MVC 발전과정(1)  (0) 2024.10.30
[Spring] 빈(Bean) 생명주기  (0) 2023.02.23
[Spring] Singleton  (0) 2023.02.03
[Spring] IOC  (0) 2023.01.26

해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.

올바르지 않은 내용이 있다면 의견 남겨주세요. 언제든지 환영입니다.

 

1. 프론트 컨트롤러

 

1-1. VER.1

 

MVC 패턴을 적용하였지만, 하나의 컨트롤러에 공통 + 비즈니스 로직이 같이 존재한다. 하지만 프론트 컨트롤러 패턴을 사용하면 웹 애플리케이션의 구조를 단순화하고, 공통된 처리 로직을 중앙 집중화할 수 있다. 스프링 MVC는 dispatcherServlet이 프론트컨트롤러 패턴으로 구현되어있다.

 

공통을 집중한 프론트 컨트롤러 패턴은 다음과 같은 장점이 있다

  • 단일 진입점에서 처리하여 공통된 처리 로직을 중앙 집중화할 수 있으므로 코드의 일관성을 유지하고, 중복된 코드를 줄일 수 있다.
  • 코드의 유지보수가 수월하다.

1-2. VER.1 의도

더보기
  • 이전 구조 최대한 유지
  • 프론트 컨트롤러는 오직 URL 매핑 정보를 확인 후 해당 URL과 상응하는 컨트롤러를 실행

 

1-3. VER.1 코드

package hello.servlet.web.frontcontroller.v1;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public interface ControllerV1 {

    /* 예외는 서블렛이랑 동일하게 한 것 */
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MemberSaveControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Member savedMember = new Member();
        savedMember.setUsername(request.getParameter("username"));
        savedMember.setAge(Integer.parseInt(request.getParameter("age")));
        memberRepository.save(savedMember);

        request.setAttribute("member", savedMember);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        request.getRequestDispatcher(viewPath).forward(request, response);
    }
}
package hello.servlet.web.frontcontroller.v1;

import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/* * 하위에 있는 다 들어옴*/
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

    /* 해당 url 호출하면 FrontControllerServletV1 이 호출 되야 하므로 */
    Map<String, ControllerV1> controllerV1Map = new HashMap<String, ControllerV1>();

    /*
     * 1. 해당 컨트롤러 호출
     * 2. 생성자 > url 주소에 해당하는 Controller 저장
     * 2-1. controllerV1Map의 url 주소와 일치하는 ControllerV1이 실행 */
    public FrontControllerServletV1() {
        controllerV1Map.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerV1Map.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerV1Map.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        System.out.println("========= FrontControllerServletV1 ==============");

        // 생성자에 일치하는 URL 찾기
        /*ControllerV1 controller = controllerV1Map.get(requestURI) 는 곧
         * ControllerV1 controller =  new MemberFormControllerV1() 와 같다 (url이 /front-controller/v1/members/new-form 일 경우) => 다형성 */
        String requestURI = req.getRequestURI();
        ControllerV1 controller = controllerV1Map.get(requestURI);

        // 만약 없으면
        if (controller == null) {
            res.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        controller.process(req, res);
    }
}

  1. URL 주소를 '/front-controller/v1/*' 로 설정하여 주소가 '/front-controller/v1/save', '/front-controller/v1/list/123' 등 '/front-controller/v1/' 로만 시작한다면 FrontControllerServletV1이 실행된다.
  2. 다형성을 이용한 controllerV1Map 생성자에서 url 주소에 해당하는 Controller를 찾는다.

 

💡1-4. 다형성

더보기

같은 자료형에 여러가지 타입의 데이터를 대입하여 다양한 결과를 얻어낼 수 있다.

여기서는 MemberSaveControllerV1, MemberListControllerV1, MemberFormControllerV1가 ControllerV1 인터페이스를 상속받고 있다. 

참고 : https://reakwon.tistory.com/48

 

[JAVA] 자바 다형성(Polymorphism) 개념부터 응용 쉬운 설명

다형성(Polymorphism) 다형성이라는 개념은 OOP에서 아주 중요한 개념이므로 모르면 OOP에 대해서 제대로 안다고 할 수 없는 개념입니다. 각 요소들이 여러 가지 자료형으로 표현될 수 있다는 것을 말

reakwon.tistory.com

 

2. VER2

 

2-1. VER.2 의도

더보기
  • 모든 컨트롤러에서 뷰로 이동하는 부분을 중복 제거
    String viewPath = "/WEB-INF/views/XXX.jsp";
    request.getRequestDispatcher(viewPath).forward(request, response);

2-2. VER.2 코드

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public interface ControllerV2 {

    /* controllerv1과 같은데 반환만 myview로 진행 */
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MemberSaveControllerV2 implements ControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        Member savedMember = new Member();
        savedMember.setUsername(request.getParameter("username"));
        savedMember.setAge(Integer.parseInt(request.getParameter("age")));
        memberRepository.save(savedMember);

        request.setAttribute("member", savedMember);
        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}
package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {

    Map<String, ControllerV2> controllerV2Map = new HashMap<String, ControllerV2>();

    public FrontControllerServletV2() {
        controllerV2Map.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        controllerV2Map.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        controllerV2Map.put("/front-controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        System.out.println("========= FrontControllerServletV2 ==============");

        String requestURI = req.getRequestURI();
        ControllerV2 controller = controllerV2Map.get(requestURI);

        // 만약 없으면
        if (controller == null) {
            res.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyView view = controller.process(req, res);
        view.render(req, res);
    }
}

 

3. VER3

 

3-1. VER.3 의도

더보기
  • 서블릿 종속성을 제거
    • Controller에서 사용되지 않는 HttpServletRequest, HttpServletResponse를 개선
    • Request를 Model로 사용하는 것이 아닌 별도의 Model 객체 만듬
  • 뷰 이름 중복 제거
    • 반복되던 ' /WEB-INF/views/' 를 제거

3-2. VER.3 코드

package hello.servlet.web.frontcontroller;

import java.util.HashMap;
import java.util.Map;

public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<String, Object>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public Map<String, Object> getModel() {
        return model;
    }

    public void setModel(Map<String, Object> model) {
        this.model = model;
    }
}
package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}
package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.HashMap;
import java.util.Map;

public class MemberSaveControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {

        // paramMap을 통해 데이터 가져오기
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));

        // 비즈니스 로직
        Member savedMember = new Member();
        savedMember.setUsername(username);
        savedMember.setAge(age);
        memberRepository.save(savedMember);

        // model에 담기
        ModelView modelView = new ModelView("save-result");
        modelView.getModel().put("member", savedMember);

        return modelView;
    }
}
package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {

    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3() {
        controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        System.out.println("========= FrontControllerServletV3 ==============");

        String requestURI = req.getRequestURI();
        ControllerV3 controller = controllerMap.get(requestURI);

        // 만약 없으면
        if (controller == null) {
            res.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 파라미터 전달
        Map<String, String> paramMap = createParamMap(req);

        ModelView view = controller.process(paramMap);
        // 이제 논리 이름 'new-form' 을 /WEB-INF/views/new-form.jsp로 만들어주어야 한다.
        // 뷰를 해결해준다는 의미로 viewResolver로 이름 지었다.
        MyView myView = viewResolver(view);

        // 전달받은 model을 화면에 내려줘야 하니까
        myView.render(view.getModel(), req, res);
    }

    private static MyView viewResolver(ModelView view) {
        return new MyView("/WEB-INF/views/" + view.getViewName() + ".jsp");
    }

    private static Map<String, String> createParamMap(HttpServletRequest req) {
        Map<String, String> paramMap = new HashMap<>();
        req.getParameterNames().asIterator().forEachRemaining(paramNames -> paramMap.put(paramNames, req.getParameter(paramNames)));
        return paramMap;
    }
}

 

 

4. VER4

 

4-1. VER.4 의도

더보기
  • 개발자의 편의성을 증대
    return new ModelView("new-form") 로 객체를 생성/반환하지 않도록 수정

4-2. VER.4 코드

package hello.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}
package hello.servlet.web.frontcontroller.v4.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;

import java.util.Map;

public class MemberSaveControllerV4 implements ControllerV4 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        // paramMap을 통해 데이터 가져오기
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));

        // 비즈니스 로직
        Member savedMember = new Member();
        savedMember.setUsername(username);
        savedMember.setAge(age);
        memberRepository.save(savedMember);
        
        // 모델 담기
        model.put("member", savedMember);

        return "save-result";
    }
}
package hello.servlet.web.frontcontroller.v4;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {

    private Map<String, ControllerV4> controllerMap = new HashMap<>();

    public FrontControllerServletV4() {
        controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
        controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
        controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        System.out.println("========= FrontControllerServletV4 ==============");

        String requestURI = req.getRequestURI();
        ControllerV4 controller = controllerMap.get(requestURI);

        // 만약 없으면
        if (controller == null) {
            res.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 파라미터 전달
        Map<String, String> paramMap = createParamMap(req);
        Map<String, Object> model = new HashMap<>();

        String view = controller.process(paramMap, model);
        MyView myView = viewResolver(view);

        // 전달받은 model을 화면에 내려줘야 하니까
        myView.render(model, req, res);
    }

    private static MyView viewResolver(String view) {
        return new MyView("/WEB-INF/views/" + view + ".jsp");
    }

    private static Map<String, String> createParamMap(HttpServletRequest req) {
        Map<String, String> paramMap = new HashMap<>();
        req.getParameterNames().asIterator().forEachRemaining(paramNames -> paramMap.put(paramNames, req.getParameter(paramNames)));
        return paramMap;
    }
}

 

5. VER5

 

5-1. VER.5 의도

더보기
  • 현재 프론트 컨트롤러는 한가지 방식의 프론트 컨트롤러 인터페이스만 사용할 수 있어 다양한 방식의 컨트롤러 인터페이스를 호환할 수 있도록 개선

5-2. VER.5 코드

package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public interface MyHandlerAdapter {

    // 컨트롤러 넘어왔을때, 나 이 컨트롤러 지원 가능해!
    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ViewResolver;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@WebServlet(name = "frontControllerServletV5",  urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
    
    // 아무 컨트롤러 다 들어갈 수 있어야 하므로
    private final Map<String, Object> handlerMappingMap = new HashMap<String, Object>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<MyHandlerAdapter>();

    public FrontControllerServletV5() {
        initHandlerMappingMap();
        initHandlerAdapters();

    }

    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    private void initHandlerMappingMap() {
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

        //V4 추가
        handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("========= FrontControllerServletV5 ==============");

        Object handler = getHandler(request);
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyHandlerAdapter adapter = getHandlerAdapter(handler);
        ModelView mv = adapter.handle(request, response, handler);
        MyView view = viewResolver(mv.getViewName());
        view.render(mv.getModel(), request, response);
    }
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
    }
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
package hello.servlet.web.frontcontroller.v5.adapter;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

// v3를 지원하는 어댑터
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4); // V3가 맞으면 true
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler; // cast

        // 파라미터 세팅
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();
        String viewName = controller.process(paramMap, model);
        ModelView modelView = new ModelView(viewName);
        modelView.setModel(model);
        return modelView;
    }

    private static Map<String, String> createParamMap(HttpServletRequest req) {
        Map<String, String> paramMap = new HashMap<>();
        req.getParameterNames().asIterator().forEachRemaining(paramNames -> paramMap.put(paramNames, req.getParameter(paramNames)));
        return paramMap;
    }
}

1) 참고 - 프론트 컨트롤러
https://f-lab.kr/insight/front-controller-pattern-20240912

 

효율적인 웹 애플리케이션 개발을 위한 프론트 컨트롤러 패턴

이 블로그 포스트는 프론트 컨트롤러 패턴의 개요와 동작 원리, 예제, 장점과 단점에 대해 다룹니다. 프론트 컨트롤러 패턴을 통해 웹 애플리케이션의 구조를 단순화하고, 코드의 유지보수성을

f-lab.kr

 

 

'Java > Spring' 카테고리의 다른 글

[Spring] JAVA 1.8 gradle 세팅  (1) 2024.12.18
[Spring] MVC 발전과정(1)  (0) 2024.10.30
[Spring] 빈(Bean) 생명주기  (0) 2023.02.23
[Spring] Singleton  (0) 2023.02.03
[Spring] IOC  (0) 2023.01.26

해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.

올바르지 않은 내용이 있다면 의견 남겨주세요. 언제든지 환영입니다.

 

1. 서블릿 코드

package hello.servlet.domain.member;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용
* */
public class MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();
    private static Long sequence = 0L; // id가 하나씩 증가하는 것 사용

    // 순수하게 만드는 것이므로 싱글톤
    private static final MemberRepository instance = new MemberRepository();
    public static MemberRepository getInstance() {
        return instance;
    }
    private MemberRepository() {}

    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }
    public Member findById(Long id) {
        return store.get(id);
    }

    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
    public void clearStore() {
        store.clear();
    }
}
package hello.servlet.web.servlet;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name= "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        Member savedMember = new Member();
        savedMember.setUsername(req.getParameter("username"));
        savedMember.setAge(Integer.parseInt(req.getParameter("age")));
        memberRepository.save(savedMember);

        res.setContentType("text/html");
        res.setCharacterEncoding("UTF-8");
        PrintWriter out = res.getWriter();
        out.write("<html>\n" +
                "<head>\n" +
                "    <meta charset=\"UTF-8\">\n" +
                "</head>\n" +
                "<body>\n" +
                "성공\n" +
                "<ul>\n" +
                "    <li>id="+savedMember.getId()+"</li>\n" +
                "    <li>username="+savedMember.getUsername()+"</li>\n" +
                "    <li>age="+savedMember.getAge()+"</li>\n" +
                "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" +
                "</body>\n" +
                "</html>");
    }
}

 

MemberRepository.java 파일에서

 

public static MemberRepository getInstance() {
    return instance;
}

 

getInstance()로 싱글톤 패턴을 구현하였으며, 스프링 빈을 이용하지 않고 순수하게 서블릿을 만들었다. 비즈니스 로직만 구현하면 되니 보다 편하게 구현할 수 있었으나, 자바코드로 html을 만들어 간단한 회원등록 화면임에도 복잡하고 비효율적인 코드를 작성할 수 밖에 없었다. 그래서 자바 코드로 html을 만드는 대신 html 문서에 자바 코드를 동적으로 넣을 수 있다면 편리하지 않을까하고 고민하여 나온 것이 템플릿 엔진이다.

 

2. JSP

<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    MemberRepository memberRepository = MemberRepository.getInstance();
    List<Member> list = memberRepository.findAll();
%>
<html>
<head>
    <title>Title</title>
</head>
<body>
<table>
    <a href="/index.html">메인</a>
    <thead>
    <th>id</th>
    <th>username</th>
    <th>age</th>
    </thead>
    <tbody>
    <%
    for (Member member : list) {
        out.write("    <tr>");
        out.write("        <td>" + member.getId() + "</td>");
        out.write("        <td>" + member.getUsername() + "</td>");
        out.write("        <td>" + member.getAge() + "</td>");
        out.write("    </tr>");
    }
    %>
    </tbody>
</table>
</body>
</html>

 

JSP로 작성하여 뷰를 생성하는 html 작업은 깔끔하게 변했지만, 하나의 파일에 비즈니스 로직과 뷰 영역 2가지가 혼잡해있다. 작은 프로젝트여서 위와 같은 코드로 마무리 했지만,  큰 프로젝트는 위와 같은 방법이라면 유지보수가 쉽지 않다.

🚨2-1. JSP

더보기

JSP는 Jakarta Server Pages로 html 내에 자바 코드를 삽입하여 웹 서버에서 동적으로 웹 페이지를 생성하여 웹 브라우저에 돌려주는 서버 사이드 스크립트 언어이다.

  • <% %> : Java 코드
  • <%= %> : 문자열, 변수, 함수 리턴 값 출력
spring boot에서는 2012년 부터 Jsp와 thymeleaf 의 비교글을 게시하였으며 이제는 jsp 대신 thymeleaf를 더 많이 사용한다. 공식문서는 애초에 jsp는 피하라고 언급하였으며 제한 사항은 다음과 같다고 서술했다.
  • JSP 지원의 한계가 존재한다. Spring Boot 애플리케이션이 실행 가능한 JAR 파일로 패키징될 때 JSP를 지원하지 않기 때문이다.
  • WAR 패키징 한다. Jetty와 Tomcat을 사용할 경우, WAR 파일로 패키징하면 JSP가 작동한다. 그러면 java -jar로 실행할 수 있으며, 표준 컨테이너에 배포할 수 있다.
  • Undertow는 JSP를 지원하지 않는다.
  • 커스텀 error.jsp 페이지를 생성해도 기본 오류 처리 뷰를 대체하지 않니다. 

즉, JSP를 사용하고 싶다면 WAR 파일로 패키징하고 Tomcat 또는 Jetty 같은 서블릿 컨테이너에 배포해야 하며, Undertow는 아예 JSP를 지원하지 않으니 주의해야 한다는 내용이다.

 

참고 1: https://docs.spring.io/spring-boot/docs/2.0.x/reference/html/boot-features-developing-web-applications.html#boot-features-jsp-limitations

 

28. Developing Web Applications

The Spring Web MVC framework (often referred to as simply “Spring MVC”) is a rich “model view controller” web framework. Spring MVC lets you create special @Controller or @RestController beans to handle incoming HTTP requests. Methods in your contr

docs.spring.io

참고 2: https://spring.io/blog/2012/10/30/spring-mvc-from-jsp-and-tiles-to-thymeleaf

 

Spring MVC: from JSP and Tiles to Thymeleaf

When it comes to the view layer, Spring @MVC gives you a variety of choices. In this article, we will first discuss the way you have most likely used the view layer in the past few years: JSP. We will see the bad and better ways to work with them (plain JS

spring.io

 

 

3. MVC

 

3-1. MVC 개념

더보기

MVC(Model-View-Controller)는 사용자 인터페이스, 데이터 및 제어 논리를 구현하는 데 일반적으로 사용되는 소프트웨어 설계 패턴이다.

  • Model : 데이터, 비즈니스 로직 관리
  • View: 레이아웃
  • Controller: 클라이언트가 요청한 명령을 모델과 뷰로 전달

 

3-2. MVC 실행 순서

더보기
  1. 클라이언트 호출하면 컨트롤러가 파라미터를 검증하고 http 요청이 맞는지 확인한다.
  2. 컨트롤러는 확인이 끝났으면 서비스, 리포지토리를 통해 결과 받는다.
  3. 컨트롤러는 서비스, 리포지토리를 통해 받은 결과를 모델에 담는다.
  4. 컨트롤러가 뷰에 제어권을 넘긴다
  5. 제어권을 받은 뷰는 컨트롤러가 모델에 담은 데이터를 참조하여 렌더링한다.
  6. 클라이언트 응답 받는다.

 

💡3-3. MVC1 vs MVC2

더보기

모든 출력을 Java 코드 내에서 문자열로 작성해야 한다는 점에서 오류가 발생하기 쉬웠다. 이때 등장한 JSP는 JAVA 코드를 삽입할 수 있도록 허용하게 했다. 그렇기에 사람들은 데이터베이스 코드, 비즈니스 로직, HTML 마크업등 모든 자바스크립트 코드를 한 곳에서 작성하기 시작했고 이것은 코드 재사용을 불가능하게 하였다. 이러한 구조를 MVC1(Model1)로 정하기도 하였다.

MVC2 패턴은 MVC1의 문제를 개선하였으며 1970년 후반 스몰토크를 연구하던 개발자들과 합의로 인해 나온 패턴이다. 구조는 위의 사진과 같다. 

 

참고 : https://stackoverflow.com/questions/796508/what-is-the-actual-difference-between-mvc-and-mvc-model2
 

What is the actual difference between MVC and MVC Model2

just i want to know What is the actual difference between MVC and MVC Model2 in the development

stackoverflow.com

 

3-4. MVC 적용 코드

⚠️ 아래 적용한 코드의 Service는 이전에 만든 MemberRepository로 사용하고,  Model은 request 내부 저장소를 이용하였다. 그리고 request.getAttribute() , request.setAttribute()를 사용하여 데이터 보관 조회하였다.

package hello.servlet.web.servletmvc;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.List;

@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 비즈니스 로직 호출
        List<Member> memberList = memberRepository.findAll();

        // Model에 데이터를 보관한다.
        // 우리는 request를 모델로 가정하였으므로 모델인 request 객체 내부 저장소에 저장하게 된다.
        req.setAttribute("memberList", memberList);

        String viewPath = "/WEB-INF/views/members.jsp";
        req.getRequestDispatcher(viewPath).forward(req, resp);
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/index.html">메인</a>
<table>
    <thead>
    <th>id</th>
    <th>username</th>
    <th>age</th>
    </thead>
    <tbody>
    <c:forEach var="item" items="${memberList}">
        <tr>
            <td>${item.id}</td>
            <td>${item.username}</td>
            <td>${item.age}</td>
        </tr>
    </c:forEach>
    </tbody>
</table>
</body>
</html>

'Java > Spring' 카테고리의 다른 글

[Spring] JAVA 1.8 gradle 세팅  (1) 2024.12.18
[Spring] MVC 발전과정(2)  (0) 2024.10.31
[Spring] 빈(Bean) 생명주기  (0) 2023.02.23
[Spring] Singleton  (0) 2023.02.03
[Spring] IOC  (0) 2023.01.26

해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.

올바르지 않은 내용이 있다면 의견 남겨주세요. 언제든지 환영입니다.

 

1. Lifecycle Callbacks

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

아래는 김영한씨의 강의의 예제 코드를 참고하여 우리가 의도하는 결과가 나오는지 테스트해보았다.

public class NetworkClientTest {

    private String url;
    
    public NetworkClientTest() {
        System.out.println("현재 url은 다음과 같습니다 : " + url);
        connect();
        call("초기화 연결되었습니다");
    }
    
    // 연결함수 
    public void connect() {
        System.out.println("connect, 연결된 url : " + url);
    }
    
    public void disconnect() {
        System.out.println("disconnect, 연결 끊어버린 url : " + url);
    }
    
    // URL 세팅 함수
    public void setUrl(String url) {
        this.url = url;
    }
    
    // 호출함수
    // @param msg : 호출하려는 메세지
    public void call(String msg) {
        System.out.println("call message : " + msg + " url : " + url);
    }
}

lifecycle 테스트 결과

나는 객체가 생성되면서, 원하는 url이 세팅되도록 의도했으나 그게 되지 않았다. 당연하다. 스프링 빈은 객체를 생성할 때 생성자 > 세터기반으로 생성하고 그 뒤에 의존관계를 주입한다. 이미 저 코드는 주입까지 모두 마친 후에 내가 초기값을 할당한게 된다. 그럼 connect() 함수가 실행 되기 전, 초기값을 세팅하고 싶으면 어떻게 해야할까? 방법은 아래 3가지로 나타낼 수 있다.

 

2. InitializingBean, DisposableBean

InitializingBean, DisposableBean 인터페이스들로 초기화 작업 및 연결을 끊은 이후의 작업을 진행할 수 있다.

public class NetworkClientTest implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClientTest() {
        System.out.println("현재 url은 다음과 같습니다 : " + url);
    }

    // 연결함수
    public void connect() {
        System.out.println("connect, 연결된 url : " + url);
    }

    public void disconnect() {
        System.out.println("disconnect, 연결 끊어버린 url : " + url);
    }

    // URL 세팅 함수
    public void setUrl(String url) {
        this.url = url;
    }

    // 호출함수
    // @param msg : 호출하려는 메세지
    public void call(String msg) {
        System.out.println("call message : " + msg + " url : " + url);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterPropertiesSet, 초기화 작업 함수");
        connect();
        call("초기화 연결되었습니다");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("destroy, 연결 파괴될 때 callback");
    }
}

InitializingBean, DisposableBean&nbsp;lifecycle 테스트 결과

다만, 위의 인터페이스들은 스프링 전용 인터페이스이며 스프링 공식문서에서는 JSR-250의  @PostConstruct @PreDestroy을 사용하길 권장한다. 해당 @은 스프링 전용 인터페이스가 아니고 자바 표준이며 스프링이 아닌 다른 컨테이너에서도 동작하기 때문이다.

 

3. @PostConstruct @PreDestroy

public class NetworkClientTest{

    private String url;

    public NetworkClientTest() {
        System.out.println("현재 url은 다음과 같습니다 : " + url);
    }

    // 연결함수
    public void connect() {
        System.out.println("connect, 연결된 url : " + url);
    }

    public void disconnect() {
        System.out.println("disconnect, 연결 끊어버린 url : " + url);
    }

    // URL 세팅 함수
    public void setUrl(String url) {
        this.url = url;
    }

    // 호출함수
    // @param msg : 호출하려는 메세지
    public void call(String msg) {
        System.out.println("call message : " + msg + " url : " + url);
    }

    @PostConstruct
    public void init() throws Exception {
        System.out.println("PostConstruct 초기화 작업 함수");
        connect();
        call("초기화 연결되었습니다");
    }

    @PreDestroy
    public void close() throws Exception {
        System.out.println("PreDestroy, 연결 파괴될 때 callback");
    }
}

@PostConstruct @PreDestroy&nbsp;lifecycle 테스트 결과

@PostConstruct, @PreDestroy가 붙은 메서드는 명시적으로 선언된 callback 메서드와 생명주기의 같은 지점에서 호출된다. @만 붙이면 되므로 매우 편리한 장점을 가지고 있다. 다만 외부 라이브러리에는 적용하지 못하므로, 외부 라이브러리를 초기화, 종료해야한다면 @Bean의 기능을 사용한다.

 

4. Receiving Lifecycle Callbacks

public class NetworkClientTest{

    private String url;

    public NetworkClientTest() {
        System.out.println("현재 url은 다음과 같습니다 : " + url);
    }

    // 연결함수
    public void connect() {
        System.out.println("connect, 연결된 url : " + url);
    }

    public void disconnect() {
        System.out.println("disconnect, 연결 끊어버린 url : " + url);
    }

    // URL 세팅 함수
    public void setUrl(String url) {
        this.url = url;
    }

    // 호출함수
    // @param msg : 호출하려는 메세지
    public void call(String msg) {
        System.out.println("call message : " + msg + " url : " + url);
    }

    public void init() throws Exception {
        System.out.println("init 초기화 작업 함수");
        connect();
        call("초기화 연결되었습니다");
    }

    public void close() throws Exception {
        System.out.println("PreDestroy, 연결 파괴될 때 callback");
    }
}

Receiving Lifecycle Callbacks lifecycle 테스트 결과

@Bean을 통해 정의된 모든 클래스는 일반적인 생명주기 콜백을 지원한다. @Bean은 Spring XML의 bean 엘리먼트 속성인 init-method, destroy-method에 초기화, 파괴용도의 콜백 메서드를 지정할 수 있다. 만약, 기본적으로 'close', 'shoutdown'이라는 이름을 가진 public 메서드를 갖고 있다면 자동적으로 콜백에 참여하게 된다. 호출하기를 원하지 않는다면 @Bean(destroyMethod="") 와 같이 추가한다. @Bean은 기본적으로 (inferred, 추론) 하기 때문이다.

 

5. Combining Lifecycle Mechanisms

빈(Bean) 생명주기 동작 방법을 3가지를 알아보았다. 만약 하나의 빈에 여러개의 생명주기 동작이 설정되었고, 각기 다른 메서드 이름으로 구성되었을 때 각 메서드는 다음과 같은 순서로 실행된다.
만일 중복된 메서드 이름이 존재한다면 한번만 실행된다.

Initialization methods Destroy methods
1. @PostConstruct 
2. InitializingBean 인터페이스에서 정의된 afterPropertiesSet()
3. 커스텀한 init()
1. @PreDestroy
2. DisposableBean 인터페이스에서 정의된 destroy()
3. 커스텀한 destroy()

 


참고 1) 김영한(스프링 핵심원리 - 기본편)

참고 2) https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-combined-effects

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

참고 3) https://johngrib.github.io/wiki/spring/document/core/01-12-java-based-container-config/#receiving-lifecycle-callbacks

 

Spring Core Technologies - 1.12. Java-based Container Configuration

 

johngrib.github.io

 

'Java > Spring' 카테고리의 다른 글

[Spring] MVC 발전과정(2)  (0) 2024.10.31
[Spring] MVC 발전과정(1)  (0) 2024.10.30
[Spring] Singleton  (0) 2023.02.03
[Spring] IOC  (0) 2023.01.26
[Spring] DI  (0) 2023.01.23

해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서를 번역하여 작성한 글입니다.

특히 공식 문서에 있어 올바르지 않은 번역 의견은 언제든지 환영합니다. 댓글 달아주세요

 

1. Singleton

프로젝트 하다보면 하나의 객체만 생성하고 그 객체를 공유할 경우가 많다. 그럴 때, 이 싱글톤 패턴을 활용해서 적용할 수 있다. 해당 싱글톤 패턴을 사용하면 객체 공유가 가능하며 여러 고객이 동시에 요청할 때 요청시마다 객체가 생성되는 것이 아니므로 메모리 낭비를 줄일 수 있다. 즉 싱글톤은 JVM당 하나의 인스턴스만 갖도록 하는 디자인 패턴인 것이다.

public class SingleObject {

   //create an object of SingleObject
   private static SingleObject instance = new SingleObject();

   //make the constructor private so that this class cannot be
   //instantiated
   private SingleObject(){}

   //Get the only object available
   public static SingleObject getInstance(){
      return instance;
   }

   public void showMessage(){
      System.out.println("Hello World!");
   }
}

singleton 참조값 일치

다만, 이런 싱글톤 패턴을 사용하기 위해서는 내부에서 private static 변수를 선언하고, 객체를 new() 로 생성할 수 없도록 생성자를 작성해주어야 한다. 이것은 작성 코드가 길어진다는 단점이 된다. 또한 값을 변경할 수 있는 필드가 있을 경우 문제가 발생한다는 큰 단점이 존재한다. 아래 코드는 김영한씨의 강의 중 유지(stateful)하게 설계되었을 때의 문제코드이다.

public class StatefulService {
    //상태를 유지하는 필드
    private int price; 
    
    public void order(String name, int price) {
       System.out.println("name = " + name + " price = " + price);
       
       //여기가 문제!
       this.price = price; 
    }
    
    public int getPrice() {
       return price;
    }
}

하나의 객체를 공유하다보니 내가 원하지 않는 결과가 나왔다. 그렇기에 싱글톤 패턴을 사용하기 위해서는 

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 가급적 읽기만 가능해야한다.
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal등을 사용한다.

2. Singleton Container

스프링 컨테이너는 우리가 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다. 싱글톤 scope로 정의한 bean에 대해 딱 하나의 인스턴스를 만드는 것이다. 그리고 이러한 싱글톤 인스턴스는 싱글톤 bean을 모아두는 캐시에 저장이 되며 요청이 오면 그 캐시에서 인스턴스를 반환해준다. 아래 그림은 스프링 공식 문서의 싱글톤 scope 동작 그림이다.

스프링 공식 문서 - singleton scope 사진

그럼, 우리는 이제 궁금해지기 시작한다. 공식 문서에서 accountDao를 수없이 작성했지만 하나만 생성될 수 있던 이유가 무엇일까

컨테이너 생성 흐름

위의 사진과 같은 흐름으로 컨테이너가 생성이 된다. 또한 scope란 속성이 싱글톤이므로 doRegisterBeanDefinitions에서 싱글톤으로 생성이 되겠다.

<!-- the following is equivalent, though redundant (singleton scope is the default); using spring-beans-2.0.dtd -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>

 

3. Java Based Singleton Container

그럼 java를 기반으로 한 싱글톤은 어떻게 생성이 되는 것일까, 

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(getMemberRepository());
    }

    @Bean
    public MemberRepository getMemberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(getMemberRepository(), getDiscountPolicy());
    }
}

다음과 같은 AppConfig.java 에서 new로 객체를 생성해주고 있기 때문이다. 특히 getMemberRepository()는 memberService(), getMemberRepository(), orderService()를 통해서 최소 3번 new로 객체생성하고 있어 궁금증이 생길 수 있다. 정답은 @Configuration으로 가능하다. (사실은 @Configuration 클래스 안에 있는 @Bean 메서드가 처리되는 것이다)


The @Bean methods in a regular Spring component are processed differently than their counterparts inside a Spring @Configuration class. The difference is that @Component classes are not enhanced with CGLIB to intercept the invocation of methods and fields. CGLIB proxying is the means by which invoking methods or fields within @Bean methods in @Configuration classes creates bean metadata references to collaborating objects. Such methods are not invoked with normal Java semantics but rather go through the container in order to provide the usual lifecycle management and proxying of Spring beans, even when referring to other beans through programmatic calls to @Bean methods. In contrast, invoking a method or field in a @Bean method within a plain @Component class has standard Java semantics, with no special CGLIB processing or other constraints applying.


공식문서에 보면, spring 컴포넌트의 @Bean 메서드는 spring이 @configuration 클래스에 있는 @Bean 메서드와는 다르게 처리된다. 차이점은 메서드나 필드 호출을 가로채기 위한 @Component 클래스에 CGLIB가 사용되지 않는다는 점이다.

 

CGLIB는 바이트 코드 생성 라이브러리로 Java 바이트 코드를 생성하고 변환하기 위한 고급 API이다. 동적 프록시 객체를 생성하고 필드 액세스를 가로채기 위해 AOP, 테스트, 데이터 액세스 프레임워크에서 사용된다고 한다.

public class ConfigurationSingleton {

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    @Test
    void configurationTest() {
        MemberServiceImpl memberServiceImpl = ac.getBean(MemberServiceImpl.class);
        OrderServiceImpl orderServiceImpl = ac.getBean(OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean(MemberRepository.class);

        System.out.println("memberServiceImpl = " + memberServiceImpl.getMemberRepository());
        System.out.println("orderServiceImpl = " + orderServiceImpl.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);

        Assertions.assertThat(memberServiceImpl.getMemberRepository()).isSameAs(memberRepository);
    }

    @Test
    void configurationDeep() {
        AppConfig appConfig = ac.getBean(AppConfig.class);
        System.out.println(appConfig.getClass());
    }
}

그래서 위와 같은 코드 작성했을 때, @Configuration 작성 유무에 따라 memberRepository 객체가 싱글톤으로 유지되는지를 확인해볼 수 있다. 결론은 싱글톤을 사용하기 위해 우리는 @Configuration-@Bean을 한 세트로 기억하도록 한다.


참고 1) 김영한(스프링 핵심원리 - 기본편)

참고 2) https://www.tutorialspoint.com/design_pattern/singleton_pattern.htm

 

Design Pattern - Singleton Pattern

Singleton pattern is one of the simplest design patterns in Java. This type of design pattern comes under creational pattern as this pattern provides one of the best ways to create an object.

www.tutorialspoint.com

참고 3) https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factorybeans-annotations

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

참고 4) 스프링 공식문서 번역한 블로그
https://johngrib.github.io/wiki/spring/document/core/01-10-classpath-scanning-and-managed-components/#1105-defining-bean-metadata-within-components

 

Spring Core Technologies - 1.10. Classpath Scanning and Managed Components

 

johngrib.github.io

 

'Java > Spring' 카테고리의 다른 글

[Spring] MVC 발전과정(1)  (0) 2024.10.30
[Spring] 빈(Bean) 생명주기  (0) 2023.02.23
[Spring] IOC  (0) 2023.01.26
[Spring] DI  (0) 2023.01.23
[Spring] Thymeleaf Layout Dialect 설정  (0) 2022.09.27

해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서를 번역하여 작성한 글입니다.

특히 공식 문서에 있어 올바르지 않은 번역 의견은 언제든지 환영합니다. 댓글 달아주세요

 

1. IOC(Inversion of Control)

제어의 역전 IoC(Inversion of Control) 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다. 하지만 프로그램의 제어 흐름을 구현 객체가 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.

 

예를 들어, 

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

위의 코드를 보면, OrderServicempl가 MemoryMemberRepository, FixDiscountPolicy란 객체를 생성하고 연결했다.

역할과 구현으로 나누었다고 하지만 OrderServiceImpl은 MemberRepository와 MemoryMemberRepository를 바라보고 있으며 또 DiscountPolicy, FixDiscountPolicy를 동시에 바라보고 있다.

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자
    public OrderServiceImpl (MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    };
public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(getMemberRepository());
    }

    private MemberRepository getMemberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(getMemberRepository(), getDiscountPolicy());
    }

    private DiscountPolicy getDiscountPolicy() {
        return new RateDiscountPolicy();
    }
}

그렇기에 AppConfig 파일을 만듦으로써, OrderServiceImpl은 자신의 로직을 실행하는 역할만 담당하고 어떤 구현 객체들이 실행될지 알 수 없어지게 수정했다. 즉 이전의 OrderServiceImpl가 가지고 있던 제어 흐름을 이제 AppConfig가 가져간 것이다. 이렇게 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)인 것이다.

 

2. Introduction to the Spring IoC Container

IoC의 원리는 의존성 주입(DI)*이다. 스프링 프레임워크는 빈(🫘) 생성 시에 컨테이너에 의존성을 주입한다. 이러한 과정은 Service Locator pattern** 과 같은 메커니즘을 사용하여 종속성의 인스턴스화 또는 위치를 제어하는 빈 자체의 역(즉, 제어의 반전)인 것이다.

 

org.springframework.beans와 org.springframework.context 패키지는 스프링 IOC 컨테이너의 기본 패키지들이다. BeanFactory 인터페이스는 객체 생성과 검색에 대한 기능을 정의하고 있다. 예를 들어 생성된 객체를 검색하는데 필요한 getBean() 메서드가 존재하며, 객체 검색 이외에도 싱글톤/프로토타입 빈인지 여부 확인하는 기능이 존재한다. ApplicationContext는 메시지 소스를 활용한 국제화 기능(한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로), 환경 변수(로컬, 개발, 운영 구분 처리), 애플리케이션 이벤트(이벤트 발행하고 구독하는 모델), 리소스조회(파일, 클래스패스, 외부)를 가지고 있는 BeanFactory의 서브인터페이스다.  

 

BeanFactory, ApplicationContext

ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 중복 오류가 발생한다")
void findBeanByParentTypeDuplicate(){
    DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
    Assertions.assertThrows(NoUniqueBeanDefinitionException.class, ()-> ac.getBean(DiscountPolicy.class));
}
@Configuration
static class TestConfig {

    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }

    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}

 

3. XML-based Container  Configuration and  Java-based Container Configuration

xml과 @을 기반으로 한 IOC 설정방법은 다음과 같다.

같은 코드의 java, xml based container configuration

요즘 많은 개발자들은 Java를 기반으로 한 스프링 설정을 많이 사용한다. XML 기반은 <bean>을 이용해 구성한다면 Java 구성에서는 일반적으로 @Configuration 클래스 내에서 @Bean을 사용한다.


* DI는 [Spring] DI로 따로 정리되어있다.

** Service Locator pattern은 디자인 패턴 중 하나로, 중개자를 통해 접근하도록 하는 패턴이다.

 

2023/01/26 : 왜 BeanFactory보다 ApplicationContext를 써야하는지 정리할 것

참고 1) 김영한(스프링 핵심원리 - 기본편)

참고 2) 최범균(초보 웹 개발자를 위한 스프링 4 프로그래밍 입문)

참고 3) https://docs.spring.io/spring-framework/docs/current/reference/html/core.html

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

 

'Java > Spring' 카테고리의 다른 글

[Spring] 빈(Bean) 생명주기  (0) 2023.02.23
[Spring] Singleton  (0) 2023.02.03
[Spring] DI  (0) 2023.01.23
[Spring] Thymeleaf Layout Dialect 설정  (0) 2022.09.27
[Spring] 정적 웹 페이지 vs 동적 웹 페이지  (0) 2022.09.20

해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서를 번역하여 작성한 글입니다.

특히 공식 문서에 있어 올바르지 않은 번역 의견은 언제든지 환영합니다. 댓글 달아주세요

 

1. Dependency Injection, DI

DI 설명

위의 코드에서 OrderServiceImpl는 현재 memberRepository, discountPolicy를 의존(Dependency)하고 있다. 그리고 생성자를 통해 OrderServiceImpl이 의존하고 있는 memberRepository, discountPolicy를 주입(Injection) 받았다. 이것을 의존성 주입이라고 한다. DI의 방법은 Constructor-based Dependency Injection 와 Setter-based Dependency Injection 2가지가 있다.

 

2. Constructor-based Dependency Injection, 생성자 기반 의존성 주입

생성자 기반 의존성 주입은 컨테이너가 여러 인수로 생성자를 호출하여 수행되며, 각각 종속성을 나타낸다. 

생성자를 이용하여 의존 객체를 주입할 때는 <constructor-arg> 태그를 사용한다. 의존 객체를 두 개 이상으로 사용할 때도

<constructor-arg>를 사용하여 순서대로 작성해주면 된다.  아래는 스프링 공식 문서에 작성되어 있는 예제이다. 

Consturctor-based Dependency Injection 예제

3. Setter-based Dependency Injection, 세터 기반 의존성 주입

set으로 시작하는 설정 메서드를 통해서 의존성 주입하는 방법이다. 해당 방법은 다음과 같은 형식을 지닌다.

  • 메서드 이름이 set으로 시작한다.
  • set뒤에는 프로퍼티* 이름의 첫글자를 대문다로 치환한 이름을 사용한다. (예: setCamelCase)
  • 한개의 파라미터를 가지며, 파라미터의 타입은 프로퍼티의 타입이다. 

아래는 스프링 공식 문서에 작성되어 있는 예제이다. 

Setter-based Dependency Injection 예제

4. Constructor-based or setter-based DI?

생성자 기반 DI와 설정자 기반 DI를 혼합할 수 있으므로 필수 종속성에는 생성자를 사용하고 선택적 종속성에는 설정 방법 또는 구성 방법을 사용하는 것이 좋다. 설정자 메서드에서 @Autowired 주석***을 사용하여 속성을 필수 종속성으로 만들 수 있지만, 인수의 프로그램적 유효성 검사를 사용하는 생성자 주입이 선호된다.


*** 스프링 빈이 없어도 동작하길 원한다면 @Autowired(return=false), public void setNoBean2(@Nullable Member member) , public void setNoBean3(Optional member)로 작성한다. @Nullable,Optional는 null, Optional.empty를 반환해준다. 


Spring 팀은 일반적으로 생성자 주입을 지지한다. 응용 프로그램 구성 요소를 불변 개체로 구현하고 필요한 종속성이 null이 되지 않도록 보장하기 때문이다. 또한 생성자가 주입한 구성 요소는 항상 완전 초기화 상태에서 클라이언트(호출) 코드로 반환된다. (객체 생성할 때 딱 1번 호출되고 이후에 호출되는 일이 없다) 부가적으로, 많은 수의 생성자 인수는 나쁜 코드이며, 이는 클래스가 너무 많은 책임을 가지고 있을 가능성이 있으며 적절한 문제 분리를 더 잘 해결하기 위해 리팩터링되어야 한다는 것을 의미한다.

또한 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없으며, 대부분 의존관계가 애플리케이션 종료전까지 변하면 안되므로 생성자 주입이 더 좋다.

세터 주입은 주로 클래스 내에서 합리적인 기본값을 할당할 수 있는 선택적 종속성에만 사용되어야 한다. 그렇지 않으면 코드가 종속성을 사용하는 모든 곳에서 null이 아닌 검사를 수행해야 한다. 세터 인젝션의 한 가지 이점은 세터 메소드가 해당 클래스의 객체를 나중에 재구성하거나 재인젝션할 수 있게 한다는 것이다. (: public으로 열어두어야 한다. 이것은 누군가 실수로 변경할 수도 있다.) 따라서 JMX MBeans를 통한 관리는 세터 주입을 위한 강력한 사용 사례이다.

 

 

 

 

 


* 프로퍼티 : 자바빈즈(JavaBeans)**에 규약된 프로퍼티를 의미한다. bean 클래스의 프로퍼티를 정의할 때는 public getter setter 메서드를 제공해야한다.

public class FaceBean {
    private int mMouthWidth = 90;

    public int getMouthWidth() {
        return mMouthWidth;
    }
    
    public void setMouthWidth(int mw) {
        mMouthWidth = mw;
    }
}

https://docs.oracle.com/javase/tutorial/javabeans/writing/properties.html

 

Properties (The Java™ Tutorials > JavaBeans(TM) > Writing JavaBeans Components)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

** JavaBeans, 자바빈

자바빈즈 규약은 재사용 가능한 객체를 빈(Bean)으로 명명하고 있으며 다음과 같은 규약을 지니고 있다.

 

1. 자바빈은 기본 패키지가 아닌 특정한 패키지에 속해야 한다.

2. 기본 생성자가 존재해야 한다. (매개변수 값이 없는 기본 생성자)

3. 멤버변수의 접근 제어자는 private여야 한다.

4. 멤버변수에 getter/setter 메서드가 존재해야 한다.

5. 4의 getter/setter 메서드는 접근 제어자가 public이여야 한다.

https://greensky0026.tistory.com/245

 

JavaBean이란? + 자바빈 규약

JavBean 이란? 뭔가 특별한 객체나 컴포넌트일 것 같지만, 그런 것이 아닙니다. 자비빈 규약을 지켜 만들어진 클래스를 'JavaBean'이라고 합니다. 자바빈 규약 1. 자바빈은 기본 패키지가 아닌 특정한

greensky0026.tistory.com

 

참고 1) 김영한(스프링 핵심원리 - 기본편)

참고 2) 최범균(초보 웹 개발자를 위한 스프링 4 프로그래밍 입문)

참고 3) https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-collaborators

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

'Java > Spring' 카테고리의 다른 글

[Spring] Singleton  (0) 2023.02.03
[Spring] IOC  (0) 2023.01.26
[Spring] Thymeleaf Layout Dialect 설정  (0) 2022.09.27
[Spring] 정적 웹 페이지 vs 동적 웹 페이지  (0) 2022.09.20
[Spring] 반복되는 코드 Lombok으로 줄여보기  (0) 2019.08.03

Thymeleaf로 tiles를 할 수 있는 thymeleaf layout dialect 설정은 다음과 같다.

 

1.  build.gradle(pom.xml)에 자신에게 맞는 코드를 추가한다.

https://mvnrepository.com/artifact/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect

 

2. 폴더구조를 다음과 같이 설정한다.

fragments에 들어가는 파일명은 자신의 템플릿 구조에 따라 설정하면 된다.

여기서는 header(script, css 링크설정한 파일), footer, nav(상단 공통메뉴), layout(header, footer, nav 설정할 layout 파일)로만 설정하였다.

 

3. 자신에게 맞는 틀을 가지고 layout.html 구성한다.

layout.html은 상단에 xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 설정해준다.

작성방법은 다음과 같다.

<!--th:replace="layout 파일 경로 :: layout 파일 별칭"-->
<!--예) th:replace="fragments/nav :: nav_nav"-->
<th:block th:replace="fragments/nav :: nav_nav"></th:block>

 

<!--layout.html -->

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<th:block th:replace="fragments/header :: header_header"></th:block>
<body id="page-top">
<!-- Page Wrapper -->
<div id="wrapper">
    <!-- Content Wrapper -->
    <div id="content-wrapper" class="d-flex flex-column">
        <!-- Main Content -->
        <div id="content">
            <!--nav-->
            <th:block th:replace="fragments/nav :: nav_nav"></th:block>
            <!--end of nav-->
            <!--content-->
            <div th:layout:fragment="content"></div>
            <!--end of content-->
            <!--nav-->
            <th:block th:replace="fragments/footer :: footer_footer"></th:block>
            <!--end of nav-->
        </div>
    </div>
</div>
</body>
</html>

 

<!--footer.html -->

<!-- Footer -->
<footer class="sticky-footer bg-white" th:fragment="footer_footer">
  <div class="container my-auto">
    <div class="copyright text-center my-auto">
      <span>Copyright &copy; Your Website 2021</span>
    </div>
  </div>
</footer>
<!-- End of Footer -->

만약, 해당 구조 잡기가 힘들다면 https://github.com/pinkdoyaji 의 demo를 내려받아서 수정한다.

 

'Java > Spring' 카테고리의 다른 글

[Spring] Singleton  (0) 2023.02.03
[Spring] IOC  (0) 2023.01.26
[Spring] DI  (0) 2023.01.23
[Spring] 정적 웹 페이지 vs 동적 웹 페이지  (0) 2022.09.20
[Spring] 반복되는 코드 Lombok으로 줄여보기  (0) 2019.08.03

정적 웹 페이지(static web page)

정적 웹 페이지는 누군가 수동으로 변경할 때까지 동일하게 유지된다 그리고 정적사이트는 항상 모든 사람에게 동일하게 보인다.( In static web pages, Pages will remain the same until someone changes it manually. And static site will always look the same to everyone.)

 

우리가 URL(http://localhost:8080/hello-static.html)을 호출한다고 했을 때,

김영한(스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술) PDF p13

1. hello-static 관련 컨트롤러를 찾는다 ▶없다

2. hello-static.html을 찾는다.

 

동적 웹 페이지(dynamic web page)

동적 웹 사이트는 방문자에 따라 다른 정보를 제공한다. 방문자의 지역, 시간, 혹은 설정 및 선호도, 웹 사이트에서 취한 행동과 같은 여러요소에 의해 방문자의 콘텐츠가 결정될 수 있으므로 보다 맞춤화된 대화형 콘텐츠를 만들 수 있다.

(dynamic website presents different information to different visitors. The content that a visitor sees can be determined by several factors, such as their location, local time, settings and preferences, and/or actions they’re taken on the website (e.g., shopping habits), making for a more tailored and interactive experience.)

 

우리가 URL(http://localhost:8080/hello-mvc)을 호출한다고 했을 때,

김영한(스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술) PDF p14

1. hello-mvc 관련 컨트롤러를 찾는다 ▶있다.

2. ViewResolver가 hello-template.html을 찾는다.

3.hello-template.html에 model에 담긴 값을 세팅하여 보여준다.

 

** ViewResolver

스프링은 특정 뷰 기술에 얽매이지 않고 브라우저에서 모델을 렌더링할 수 있도록 ViewResolver를 제공한다. 스프링은 뷰를 처리하는 방식에 중요한 두개의 인터페이스 ViewResolver, View를 제공한다. ViewResolver는 뷰 이름과 실제 뷰 간의 매핑을 제공한다. 

(Spring provides view resolvers, which enable you to render models in a browser without tying you to a specific view technology. The two interfaces which are important to the way Spring handles views are ViewResolver and View. The ViewResolver provides a mapping between view names and actual views)

 

<bean id="viewResolver"
      class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

UrlBasedViewResolver가 있다. 위와 같이 설정하면 return "test" 시 /WEB-INF/jsp/test.jsp로 보낼 RequestDispatcher 로 전달한다.

(When returning test as a viewname, this view resolver will hand the request over to the RequestDispatcher that will send the request to /WEB-INF/jsp/test.jsp.)

 

 

 

참고 ①: https://blog.hubspot.com/website/static-vs-dynamic-website

 

Static vs. Dynamic Websites: Here's the Difference

Learn the difference between static and dynamic websites, so you can decide which approach is best for your scaling business.

blog.hubspot.com

참고 ②: 김영한(스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술)

참고 ③: https://docs.spring.io/spring-framework/docs/3.0.0.M3/reference/html/ch16s05.html

'Java > Spring' 카테고리의 다른 글

[Spring] Singleton  (0) 2023.02.03
[Spring] IOC  (0) 2023.01.26
[Spring] DI  (0) 2023.01.23
[Spring] Thymeleaf Layout Dialect 설정  (0) 2022.09.27
[Spring] 반복되는 코드 Lombok으로 줄여보기  (0) 2019.08.03

1. Lombok은 무엇일까?

 

Getter/Setter 메소드, toString() 혹은 생성자 함수를 만드는 이 반복된 작업을 Lombok을 이용해 자동화 하는 라이브러리 입니다. 자동화함으로써 개발 시간을 단축할 수 있다는 장점이 있습니다.

 

 

2. Eclipse Lombok 설치 

 

- https://projectlombok.org/download 에 접속합니다.

 

 

- 다운로드합니다.

  저는 사진에서 네모로 표시한, older versions로 들어가 lombok-1.18.2.jar를 다운받았습니다. 

 

- 명령 프롬프트를 켜주세요

 

 

 

- lombok-1.xx.x.jar이 다운받아진 주소를 복사하신 후, 

  cd 명령어 뒤에 복사한 주소를 붙여주세요.

 

 

 

- java -jar lombok-1.xx.x.jar 명령어 실행시켜주세요.

** 굳이 cd 명령어로 해당 위치 들어가지 않고, java -jar (Lombok이 있는 위치)\lombok-1.xx.x.jar으로 실행시켜주셔도 상관없습니다.

 

 

 

- 명령을 실행시키면 위와 같은 모습이 보입니다. 

  specify location 버튼을 클릭하여 Lombok을 사용할 이클립스, 혹은 스프링 프레임워크 선택해 주세요.

  선택하셨으면 Install/ Update 버튼 클릭해주세요

 

- 이클립스 혹은 스프링 프레임워크를 실행시켜주세요.

  만약 선택한 이클립스나 스프링 프레임워크가 켜져 있었다면 다시 껐다 켜주시면 됩니다.

  

- Lombok을 사용할 프로젝트 lib 폴더 안에 lombok-1.xx.x.jar를 넣어주세요

 

여기까지가 Eclipse에 Lombok을 설치하는 방법입니다.

 

 

3. Lombok vs nonLombok 

 

Lombok을 썼을 때와 쓰지 않았을 때의 차이는 다음과 같습니다.

 

 

어떤가요? 코드가 확 줄어들었습니다.

getter/setter 메서드를 어노테이션 @Getter/Setter로 바꿈으로써 코드 길이가 줄어든 것입니다.

 

 

쓰는 방식도 우리가 Lombok을 사용하지 않았을 때처럼 사용하시면 됩니다.

 

Lombok 어노테이션은 @Getter/Setter 말고도 @Data, @NonNull등 다양하게 있습니다.

https://projectlombok.org/ 페이지에 접속하시면 4분 정도의 짧은 데모 영상과 함께 어노테이션에 대한 설명도 찾아볼 수 있습니다.

'Java > Spring' 카테고리의 다른 글

[Spring] Singleton  (0) 2023.02.03
[Spring] IOC  (0) 2023.01.26
[Spring] DI  (0) 2023.01.23
[Spring] Thymeleaf Layout Dialect 설정  (0) 2022.09.27
[Spring] 정적 웹 페이지 vs 동적 웹 페이지  (0) 2022.09.20

+ Recent posts