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. DispatcherServlet

 

1-1. 개념

  • SpringMVC 프레임워크에서 FrontController 패턴으로 만들어진 Servlet이다.
  • doDispatch()
    • mappedHandler = this.getHandler(processedRequest): 핸들러를 조회한다.
    • HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()): 핸들러를 처리할 수 있는 어댑터이다.
    • ha.handle(processedRequest, response, mappedHandler,getHandler()): 핸들러 어댑터 실행 후 ModelAndView를 반환한다.
    • render(mv, request, response): 뷰 렌더링 호출한다.

1-2. 코드

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	HttpServletRequest processedRequest = request;
	HandlerExecutionChain mappedHandler = null;
	ModelAndView mv = null;
 
	// 1. 핸들러 조회
	mappedHandler = getHandler(processedRequest);
	if (mappedHandler == null) {
		noHandlerFound(processedRequest, response);
		return;
	}
 
	// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
	HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
 
	// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
 
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

	// 뷰 렌더링 호출
	render(mv, request, response);
}

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);
}

 

2. @RequetMapping

 

2-1. 개념

  • 요청 정보 매핑 하며 방법이 2가지 존재한다.
    • pathPattern : 스프링 5.3부터 MVC에서 사용할 수 있도록 설정된 방법으로  6.0에서는 기본적 사용가능하다.
      • "/resources/ima?e.png" : 경로 세그먼트에서 한 문자 일치
      • "/resources/*.png" : 경로 세그먼트에서 0개 이상의 문자와 일치합니다.
      • "/resources/**" : 여러 경로 세그먼트 일치
      • "/projects/{project}/versions" : 경로 세그먼트를 일치시키고 변수로 캡처합니다.
      • "/projects/{project:[a-z]}/versions"+ : 변수를 정규 분포와 일치시키고 캡처합니다.
    • antPathMatcher: 문자열 패턴이 정확하게 문자열 경로 일치했을 때 사용되는 방법이다. 효율성이 떨어진 단점이 있다.
  • GET/POST 요청 둘다 허용된다.

2-2. 코드

@RestController
@RequestMapping("/persons")
class PersonController {

	@GetMapping("/{id}")
	public Person getPerson(@PathVariable Long id) {
		// ...
	}

	@PostMapping
	@ResponseStatus(HttpStatus.CREATED)
	public void add(@RequestBody Person person) {
		// ...
	}
}

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-requestmapping.html

 

Mapping Requests :: Spring Framework

A reflected file download (RFD) attack is similar to XSS in that it relies on request input (for example, a query parameter and a URI variable) being reflected in the response. However, instead of inserting JavaScript into HTML, an RFD attack relies on the

docs.spring.io

3. @Controller

 

3-1. 개념

  • Controller임을 나타내며 @Component, 즉 spring의 빈으로 등록하기 위한 기본 어노테이션의 확장 형태이다. 그렇기에 @Controller만 작성해도 spring의 빈으로 등록되며 @RequestMapping 같이 사용해 특정 URL 요청 처리하는 핸들러 메서드를 정의한다.
  • @Controller = @Component + @RequestMapping

 

3-2. ❓미해결

더보기

@Controller 없으면 dispatcher-servlet.xml로 설정 하는 과정이 개발자를 고단하게 만든다는 건 이해. 그러나 @Component + @RequestMapping으로 하면 굳이 @Controller가 왜 필요한지 이해 되지 않음. ChatGPT는 웹 컨트롤러를 좀 더 명확히 구분하고 Spring MVC 역할 표현하기 위해 생겼다는데 개발자가 명명규칙을 명확히 작성하는 것과 연관있는건가 싶음.

 

3-3. 💡@RestController vs @Controller

@RestController = @Controller + @ResponseBody이다. 그래서 클라이언트가 요청한 데이터를 JSON, XML등 형식으로 반환해야 하는 REST API에서  자주 사용된다. 만약 뷰를 반환하고 싶으면 @Controller, 그게 아니라면 @RestController 사용한다.

 

특성 @Controller(Spring MVC) REST API(Spring REST)
기본목적 HTML 포함한 웹 페이지 반환 데이터(보통 JSON 또는 XML)만 반환 
HTTP 메서드 GET, POST, PUT, DELETE등 GET, POST, PUT, DELETE등
주로 사용되는 @ @Controller, @RequestMapping @RestController, @RequestMapping
주요목적 웹 애플리케이션에서 사용자 인터페이스 렌더링 RESTful API(REST API를 더 강조한 표현)의  서비스 데이터 처리

 

3-4. 코드

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

	/**
	 * Alias for {@link Component#value}.
	 */
	@AliasFor(annotation = Component.class)
	String value() default "";

}

 

4. ModelAndView

 

4-1. 개념

  • Model + View, 즉 모델과 뷰를 모두 담을 수 있는 객체다.
  • ModelAndView mv = new ModelAndView("form") 으로 뷰 이름
    modelAndView.addObject("user", user); 로 모델 데이터 담는다.
  • @Controller 사용시에 이용한다. @RestController는 ResponseEntity 또는 Java 객체 직접 반환한다.

 

5. RequestParam

 

5-1. 개념

  • Http 요청 파라미터 받는 @이다.
  • @RequestParam("username") 과 request.getParameter("username") 동일하다.

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

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

 

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

해당 내용은 노마드 코더의 강의 및 React 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.

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

 

1. Movie 컴포넌트

import PropTypes from 'prop-types';

function Movie({key, title, image, summary, genres}) {
    return(
        <div key={key}>
        <h2>{title}</h2>
        <img src={image}/>
        <p>{summary}</p>
        <ul>
          {genres.map((genre, index) => <li>{genre}</li>)}
        </ul>
      </div>
    )
}

Movie.propTypes = {
    image: PropTypes.string.isRequired,
    key:PropTypes.number.isRequired,
    title:PropTypes.string.isRequired,
    summary:PropTypes.string.isRequired,
    genres:PropTypes.arrayOf(PropTypes.string).isRequired
}

export default Movie;

components/Movie.js

 

import { useState, useEffect } from 'react';
import Movie from '../components/Movie';


// reactRouter -> 페이지 전환하는 
function Home() {
  const [loading, setLoading] = useState(true);
  const [movies, setMovies] = useState([]);

  const getMovie = async() => {
    const response = await fetch("https://yts.mx/api/v2/list_movies.json?minimum_rating=8.8&sort_by=year");
    const json = await response.json();
    setMovies(json.data.movies);
    setLoading(false);
  }

  useEffect(() => {
    // 과거에 쓰는 방법
    // fetch("https://yts.mx/api/v2/list_movies.json?minimum_rating=9&sort_by=year")
    // .then((response) => response.json())
    // .then((json) => {
    //   setMovies(json.data.movies);
    //   setLoading(false);
    // });

    getMovie();
  }, []);

  return (
    <>
      <h1>Movie</h1>
      { loading? (
        <h1>'Loading..'</h1>
        ) : 
        (
          movies.map((item) => 
          <Movie key={item.id} title={item.title} image={item.medium_cover_image} summary={item.summary}
          genres={item.genres}/>
          )
        )
      }
    </>
  );
 
}

export default Home;

routes/Home.js

 

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    
  );
}

export default App;

App.js

 

App.js는 비워두고 처음 시작시 Movie 리스트가 보여지고, url 변경 시 detail을 보여줄 수 있도록 수정한다. 이것을 위해 react-router를 이용한다.

 

2. react-router

"클라이언트 측 라우팅",  기존 웹사이트에서 브라우저는 사용자가 링크를 클릭하면 새 페이지에 대한 프로세스가 처음부터 시작된다. 반면, 클라이언트 측 라우팅을 통해 앱은 서버에서 다른 문서를 요청하지 않고도 링크 클릭을 통해 URL을 업데이트 할 수 있다. 

브라우저가 완전히 새로운 문서를 요청하거나 다음 페이지에 대한 CSS, JS 자산을 재평가할 필요가 없기 때문에 더 빠른 사용자 경험이 가능하다.

react 와 <a> 렌더링 차이

 

그러면 제목 클릭 시, Detail 페이지로 넘어가는 코드를 작성해본다.

import logo from './logo.svg';
import './App.css';
import { Route, Routes, BrowserRouter } from "react-router-dom";
import Home from './routes/Home';
import Detail from './routes/Detail';


function App() {
  return (
    <div>
      <h1>Movie Router</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/movie/:thisismovieId" element={<Detail />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

App.js

 

import PropTypes from 'prop-types';
import { Link } from "react-router-dom";

function Movie({id, title, image, summary, genres}) {
  console.log(`${id} :: title is ${title}`);
    return(
        <div key={id}>
          <h2><Link to={`/movie/${id}`}>{title}</Link></h2>
          <img src={image}/>
          <p>{summary}</p>
          <ul>
            {genres.map((genre, index) => <li key={index}>{genre}:: {index}</li>)}
          </ul>
      </div>
    )
}

Movie.propTypes = {
    image: PropTypes.string.isRequired,
    id:PropTypes.number.isRequired,
    title:PropTypes.string.isRequired,
    summary:PropTypes.string.isRequired,
    genres:PropTypes.arrayOf(PropTypes.string).isRequired
}

export default Movie;

Movie.js

 

import { useEffect } from "react";
import { useParams } from "react-router-dom";

const getDetail = async(id) => {
    const response = await fetch(`https://yts.mx/api/v2/movie_details.json?movie_id=${id}`);
    const json = await response.json();
    console.log(json);
}
function Detail() {

    // 전달한 param 가져오기
    // <Route path="/movie/:thisismovieId" element={<Detail />} /> thisismovieId가 찍히게 된다.
    const { thisismovieId } = useParams();
    console.log(thisismovieId);

    useEffect(()=> {
        getDetail(thisismovieId);
    }, []);
    return (<h1>Detail!</h1>)
}

export default Detail;

Detail.js

 

  • <Routes> : <Routes>는 모든 하위 경로를 탐색하여 가장 적합한 일치 항목을 찾아 렌더링한다. 
  • <Route>
    • 라우터 생성 함수에 전달되는 개체
    • <Route path = "/teams/:teamId">
      • path는 URL과 일치하는 경로이다. URL, href 와 일치하는지 확인한다. 
      • : 는 동적세그먼트다. 경로가 URL과 일치하면 동적 세그먼트는 URL에 파싱한다. 이 세그먼트는 useParams() 에서 가져올 수 있으며 - 는 사용할 수 없다.
        🚫 "/ teams-:teamId" 
        🚫 "/:category--:productId" 

'Web > React' 카테고리의 다른 글

[ReactJS로 영화 웹 서비스 만들기] 기본문법  (0) 2024.10.17

해당 내용은 노마드 코더의 강의 및 React 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.

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

 

1. Props

<!DOCTYPE html>
<html>
	<body>
		<div id="root"></div>
	</body>
	<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
	<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
	<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
	<script type="text/babel">
		const root = document.getElementById("root");

		// prop: 부모 컴포넌트로부터 자식 컴포넌트에 데이터를 보낼 수 있게 해주는 방법
		// 버튼을 N개 만든다고 해보자, 이때 버튼의 스타일이 동일하고 글자만 다르다면, TEXT만 바꿔서 만들 수 있도록 수정할 수 있어야 한다.
		function Button({berry, onClick}) {

			console.log(`${berry} was rendered`);
			// button컴포넌트에 던진 berry라는 key 가져오기
			// console.log(props.berry);
			// return(
			// 	<button>{props.berry}</button>
			// )

			// shortcut: Button({berry})를 통해 Button(props)의 props.berry 대신 편하게 쓴다
			// props로 받아온 onClick을 return을 통해 실제 이벤트 리스터 등록했다.
			return (
				<button onClick={onClick}>{berry}</button>
			)
		}
		

		function App() {
			const [value, setValue] = React.useState("burabong!");
			const handleClick = () => {
				setValue("cute burabong🍊");
			}
			return (
				<>
					<Button berry={value} onClick={handleClick}/>
					<Button berry="banana"/>
				</>
			)

			//<Button berry="{value}" onClick={handleClick}/> 의 onClick은 이벤트 리스너가 아니다. berry와 마찬가지로 props다.
		}
	
		ReactDOM.render(<App/>, root); 
	</script>
</html>

부모에서 자식으로 넘기는 props

props를 통한 렌더링 문제

  1. 처음 실행 시 <button> 컴포넌트를 통해 렌더링
  2. 버튼 클릭 시 <button> 재렌더링

props 타입 정리

 

협업 진행 시 해당 props 데이터는 문자만 와야하고, 필수로 넘겨주어야 할 부분들이 존재할 수 있다. 이때, PropsTypes를 이용하여 해당 key의 타입이 어떤 건지 정의해주면 개발자 도구에서 오류로 안내해준다.

 

https://classic.yarnpkg.com/en/package/prop-types

 

prop-types

Runtime type checking for React props and similar objects.

classic.yarnpkg.com

만일 사용하고 싶다면, react 설치 후 yarn add prop-types 명령어로 설치하면 사용 가능하다.

 

2. useEffect

import logo from './logo.svg';
import './App.css';
import Button from './Button';
import { useState, useEffect } from 'react';

function App() {

  const [counter , setCounter] = useState(0);
  const [keyword, setKeyword] = useState('');
  const handleClick = () => (setCounter((prev) => prev + 1));
  const handleChange = (e) => {
    setKeyword(e.target.value);
  }
  // 내가 state를 변경할 때마다 모든 컴포넌트가 재 render 됨. 하지만 일부 컴포넌트는 재 렌더링 되길 원하지 않음
  // useEffect 첫번쨰 인자 : 우리가 한번만 실행시키고 싶은 코드, 
  // 우리가 코드가 딱 한번만 실행할 수 있도록 보호하는 useEffect, state가 변화하든 무슨일이 있어도 1번
  useEffect(() => {
    console.log(`call the api`);
  }, []);

  useEffect(() => {

    // 3.keyword가 빈값이 아니고 5단어가 되면 해당 로그 찍어줘
    if(keyword !== '' && keyword.length > 4) console.log(`useEffect keyword is  ${keyword}`);
  }, [keyword]) // 2. keyword가 변경될 때만 해당 log 찍어줘, 그럼 [] 문자열 쓰면 판단할게 없으니 단 한번만 나오겠지
  
  useEffect(() => {
    console.log(`i run when "counter" change`);
  }, [counter]);

  useEffect(() => {
    console.log(`i run when "keyword & counter" change`);
  }, [keyword, counter]); // 4. keyword || counter 이다. 
  
  // 1. 입력할 때마다 재실행되고 있음, 낭비!
  //console.log(`keyword is  ${keyword}`);

  return (
    <>
      <input type="text" placeholder='search' value={keyword} onChange={handleChange}/>
      <div>Click me {counter}</div>
      <button onClick={handleClick}> Click me!</button>
    </>
  );
}

export default App;

App.js

 

 

  • useEffect(effect: React.EffectCallback, deps?: React.DependencyList)
    • effect: useEffect의 첫번째 인자. 정리 함수를 반환할 수 있는 명령형 함수. 해당 함수를 통해 우리는 딱 한번만 실행시키고 싶은 내용을 작성한다.
    • dependencyList : useEffect의 두번째 인자. [ ] 적힌 값이 변경되는 경우에만 effect 함수가 실행된다. 아무것도 안적으면 최초 1번, [a, b] 둘다 적으면 a or b와 같다. a만 변경되거나 b만 변경되었을때도 해당 effect 함수가 실행된다.
import logo from './logo.svg';
import './App.css';
import Button from './Button';
import { useState, useEffect } from 'react';

function Hello() {

  // useEffect의 생성, 해지 되었을 때 이런식으로 동작함을 파악할수 있다.

  useEffect(() => {
    console.log('created :)');
    return () => console.log('destoryed :/');
  }, []);
  return(<h1>Hello</h1>)
}

function App() {

  const [showing, setShowing] = useState(false);
  const handleClick = () => (setShowing((prev) => !prev));

  return (
    <>
      {showing ? <Hello/> : null}
      <button onClick={handleClick}>{showing ? 'Hide' : 'Show'}</button>
    </>
  );
}

export default App;

useEffect 생성, 해지

해당 내용은 노마드 코더의 강의 및 React 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.

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

 

1. JS vs React

<!DOCTYPE html>
<html>
	<body>
		<span>Total clicks: 0</span>
		<button id="btn">click me</button>
	</body>
	<script>
		let counter = 0;
		const button = document.getElementById("btn");
		const span = document.querySelector("span");
		function handleClick() {
			counter = counter + 1;
			span.innerText = `Total clicks: ${counter}`;
		}
		button.addEventListener("click", handleClick)
	</script>
</html>

count Javascript 코드

<!DOCTYPE html>
<html>
	<body>
		<div id="root"></div>
	</body>
	<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
	<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
	<script>
		// 어려운 방식 react의 동작방법을 알기 위한 방식
		const root = document.getElementById("root");
		// reactJS는 유저에게 보여질 내용을 컨트롤 할 수 있음
		// reactJS는 javascript로 그리고 html로 번역
		//const span = React.createElement("span", {id: "rabong-span", style: {color: "orange"}}, "hello i'm burabong");
		// reactdom은 reactelement를 html로 바꿔준다, reactJs는 엔진
		
		const span = React.createElement("span", null, "hello i'm burabong");
		const button = React.createElement("button", {
			onClick:() => console.log('im clicked'),
			style: {
				backgroundColor: 'yellow'
			}
		}, "Click me"); // {} 프로퍼티에 이벤트도 등록 가능하다.
		const container = React.createElement("div", null, [span, button]); // div 안에 span, button 같이 들어가기 위함
		
		ReactDOM.render(container, root); // container root안에 렌더링 해줘

	</script>
</html>

버튼 클릭 시 숫자세기 - 복잡한 React 코드

 

JavaScript 처럼 html을 생성하고 그 다음 이벤트를 추가하는 것이 아닌, React.createElement()를 통해 html 생성 및 프로퍼티, 이벤트를 한번에 만들고 있음을 확인 할 수 있다.

<!DOCTYPE html>
<html>
	<body>
		<div id="root"></div>
	</body>
	<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
	<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
	<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
	<script type="text/babel">
		const root = document.getElementById("root");

		// 이렇게 쓰는 방식을 JSX
		// 브라우저가 JSX를 이해하기 위해서 Babel 이용해 변환해준다
		function Span() {
			return (<span>hello i'm burabong</span>);
		}
		// const Span = () => (<span>hello i'm burabong</span>); 위와 똑같음
		const Button = () => (<button style={{
				backgroundColor: 'yellow'
			}} onClick={() => console.log('im clicked')

			}>Click me</button>);
		
		// 컴포넌트의 첫글자는 무조건 대문자, 안그러면 react, jsx는 html 태그라고 인식한다.
		const Container =() => (
			<div> 
				<Span/>
				<Button/> 
			</div>
		)
		
		ReactDOM.render(<Container/>, root); // container root안에 렌더링 해줘
	</script>
</html>

버튼 클릭 시 숫자세기 - JSX를 이용한 React 코드

  • JSX : JavaScript를 확장한 문법, HTML과 비슷하게 마크업을 작성할 수 있도록 해준다. 또한 CamelCase로 작성해야 하며 변수명에 대시를 포함하거나 class처럼 예약어를 사용할 수 없다. 다만 aria-*, data-* 어트리뷰트는 HTML과 동일하게 대시 사용하여 작성할 수 있다.
  • Babel: JavaScript 컴파일러, 주로 EMCAScript 2015+ 코드를 이전 버전의 JavaScript로 변환하는 데 사용되는 도구 체인이다. 이런 Babel은 JSX도 변환해줄 수 있다.

개발자 도구 통해서 본 BABEL 컴파일
버튼 클릭 시 숫자세기 - 좋은 코드①

 

<!DOCTYPE html>
<html>
	<body>
		<div id="root"></div>
	</body>
	<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
	<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
	<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
	<script type="text/babel">
		const root = document.getElementById("root");

		// state는 데이터가 기본적으로 저장되는 곳
		function App() {
			
			console.log(React.useState(0));
			// 배열을 한번에 빼내기 위해서
			// modifier(=setCounter)가지고 state(=counter)를 변경할 때 컴포넌트 재 생성, 새로운 값 가지고 리렌더링되는 것
			// 데이터가 바뀔 때마다 컴포넌트 리렌더링하고 UI refreash하는 거다 
			let [counter, setCounter] = React.useState(0);
			const onClick=()=> {
                // counter = counter + 1은 내가 원하는 값이 나오지 않을 수 있다. 다른 곳에서 써버릴 수 있으니까
				// 이전값을 가지고 값 변화해주고 싶다면 함수를 이용한다.
				setCounter((current) => current + 1); 
			}
			return (
				<div> 
					<span>Total clicks: {counter}</span>
					<button id="btn" onClick={onClick}>click me</button>
				</div>
			)
		}
	
		ReactDOM.render(<App/>, root); 
	</script>
</html>

 

  • useState:
    • 컴포넌트에 state 변수를 추가 할 수 있는 React Hook
    • useState(InitialState), 해당 결과값이 배열이므로 const [age, setAge] = React.useState(28); 처럼 배열 구조 분해를 사용하여 변수의 이름 지정하는 것이 규칙이다.
    • 정확히 두개의 값을 가진 배열 반환한다. 첫번째는 초기 state값, 두번째는 다른 값으로 업데이트하고 리렌더링을 촉발할 수 있는 set 함수다. set 함수는 반환값이 없다.
    • 반복문이나 조건문 안에서는 호출할 수 없다.

2. Converter 

<!DOCTYPE html>
<html>
	<body>
		<div id="root"></div>
	</body>
	<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
	<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
	<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
	<script type="text/babel">
		const root = document.getElementById("root");

		function App() {
			const [amount, setAmount] = React.useState(0);
			const [inverted, setInverted] = React.useState(false);


			// 입력한값으로 변경시켜 주고 싶어
			const onChange = (e) => {
				console.log(e.target.value);
				setAmount(e.target.value);
			}
			const onReset = () => setAmount();
			const onFlip = () => {
				onReset();
				setInverted((current) => !current);
			}
			return (
				<div> 
					<h1>Super Converter</h1>
					<label htmlFor="minutes">Minutes</label>
					<input value={inverted? amount * 60 : amount} id="minutes" placeholder="Minutes" type="number" onChange={onChange} disabled={inverted}/>
					<h1>you want convert {amount}</h1>
					<label htmlFor="hours">Hours</label>
					<input value={inverted? amount : Math.round(amount/60)} id="hours" placeholder="Hours" type="number" disabled={!inverted} onChange={onChange}/>
					<button onClick={onReset}>reset</button>
					<button onClick={onFlip}>Flipped</button>
				</div>
				
			)
			// label을 input과 연결시키기 위해 for="(inputId)" 그러면 label 클릭시 input 활성화가 된다.
		}
	
		ReactDOM.render(<App/>, root); 
	</script>
</html>

 

converter - react①

'Web > React' 카테고리의 다른 글

[ReactJS로 영화 웹 서비스 만들기] 기본 기능  (1) 2024.10.23

해당 내용은 codeit 강의 및 w3schools 번역을 참고하여 작성한 글입니다.

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

 

1. TypeScript 기본형 작성방법

// : string과 같은 타입을 생략해도 된다. 
// TypeScript는 처음에 세팅된 값의 타입이 아닌 값을 변경시에 오류를 보여준다.

let itemName: string = '펭수';
let itemPrice: number = 129000;
let membersOnly: boolean = true;
let owner: undefined = undefined;
let seller: null = null;

TypeScript 기본형

2. 배열과 튜플

// 배열 선언
const carts: string[][] = [
    ['c001', 'c002']
];
const current :[number, number] = [0, 0];
const items: string[] = [
    '갑옷' ,'빨간 물약'
];

// 튜플(길이와 각 요소마다의 타입이 고정된 배열) 선언
let myTuple : [number , boolean , string];
let mySize: [number, number, String] = [5,6,'55'];
mySize.push(0,0,'11');
console.log(mySize);

배열과 튜플

mySize.push('1234');

// 위와 같이 작성하면 mySize에 [ 5, 6, '55', '1234' ]로 추가가 됨을 확인할 수 있다.
// 이런 내용을 방지하고자 'readOnly'를 추가하면 위와 같이 추가가 불가능하다.

let ourTuple: readonly [number, number, string] = [5, 6, '55'];
ourTuple.push('1234');

// prog.ts(10,10): error TS2339: Property 'push' does not exist on type 'readonly [number, number, string]'.

 

3. TypeScript 객체

let product: {
    id: String;
    name: String;
    sizes: String[];
    membersOnly?: boolean; //?는 optional property
} = {
    //id: 1 이렇게 하면 해당 오류 알려준다
    id: 'c001',
    name: '코드잇 블랙 후디',
    sizes: ['M', 'L', 'XL']
}

optional property는 필수로 정의하지 않아도 되는 속성이다. 위의 코드에서 memeberOnly 뒤에 ?를 표시하였는데, 이건 필수로 정의하지 않아도 된다. 만약 없다면 오류 발생한다. 

// 속성 정의되지 않았을 때 쓰는 방법
// index 대신 어떤 이름이든 상관 없다. 다만 속성명은 String이고 속성 값이 number라는 정의만 지키면 된다.
const nameAgeMap: { [index: string]: number } = {};

nameAgeMap.Jack = 25; // no error
nameAgeMap.Mark = "Fifty"; // Error: Type 'string' is not assignable to type 'number'.

console.log(nameAgeMap);

 

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

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

 

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

+ Recent posts