- Undo : 실행한 내용을 취소 update, insert delete 취소, 언두 테이블 스페이스가 있음

- Redo : 다시 실행하기, 장애 시에 다시 실행(update, insert delete를 다시 실행), 최신 데이터로 복구

 

- 데이터는 datafiles에 저장됨 (스토리지에 저장)

- Control File : DB의 구조 정보 

- Online redo log files : 리두로그가 파일 형태로 저장 (변경 작업 내용 기록)

 

- DB를 start를 하면 Buffer Cache에 데이터가 올라감

- SGA : 오라클이 쓰는 메모리 영역

- SGA에 Redo log buffer라는 영역이 있음

 

 - Undo : Data files에 Undo Tablespace가 있음

- DML을 실행하면 변경내용은 일단 Buffer Cache에 기록

- Buffer Cache → Data files 적용은 commit시점에 반영되지는 않고, 내부적으로 체크포인트가 발생할때 반영함(DBWR) (커밋할때마다 디스크에 기록하면 느려질 수 있음)

 

- Undo Tablespace에 변경전 내용을 저장함

- 다른 사람들은 DML실행자가 commit하기 전까진 변경전 데이터를 봐야함 (변경전 내용은 Undo에 있음)

- 데

 

- Redo는 복구를 위해 필요함

- DML문장을 Redo log buffer에 기록함, commit을 하는 순간 로그를 디스크(Online redo log files)에 기록함(LGWR)

 

- Online redo log files은 항상 최신이고, Data files은 최신이 아닐 수 있음

- Online redo log files과 Data files 각 SCN 을 비교해서, 반영이 되지 않은 내용을 파악하고 Online redo log files에 있는 내용을 바탕으로 Data files에도 반영하여 최신화 해줌

 

- Archive Mode로 운영한다면, Online redo log files을 또 Archive log files로 백업 보관함(ARCH)

 


 

- 롤백 세그먼트 = Undo

- DML을 한 경우 변경전 데이터를 저장함 (Undo Tablespace)

- Undo는 저장공간을 재활용해서 사용함

- 저장공간을 재활용해서 쓰므로, 변경작업이 너무 많은 작업을 Commit을 안하면, 예전 데이터는 날라갈 수 있음(ORA-01555 : snapshot too old  에러)

- Undo Tablespace 공간을 늘리거나, Commit을 중간중간 해주기

 


 

- Group : 한 그룹에 여러개의 Member로 이중화 (Member끼리는 같은 내용)

- Redo log files은 Archive Log Files로 백업 보관함 (ARCH)

'DATABASE > oracle' 카테고리의 다른 글

Parallel Processing  (0) 2023.06.04
[Oracle] 9. REDO와 UNDO  (0) 2021.07.23
[Oracle] 3. 캐시와 공유 메모리  (0) 2021.07.12
[Oracle] 2. 오라클의 여러 프로세스  (0) 2021.07.12
[Oracle] 1. I/O와 디스크의 관계  (0) 2021.07.12

 

- 사용자의 세션이 하나 붙으면 한 개의 프로세스로 1:1로 붙어서 처리함

- SQL이 느린데.. 더 이상 튜닝을 할 여지가 없는 경우 parallel을 사용하는 방법이 있음

- 하나의 SQL 실행을 위해서 여러개의 프로세스를 띄어서 하나의 작업을 나누어서 처리해서 빠르게 함

- 여러 프로세스 결과 취합을 위해 Query Coordinator라는 게 있음 (QC)

- 여러 프로세스가 처리한 내용을 QC에 보내서 최종 결과를 반환함

 

- 쓰는 방법 : select /*+ parallel(4) */ count(*) from emplyee;

   → 4개의 프로세스를 띄어서 해당 SQL을 처리함

 

- DML 병렬처리도 가능함 : enable_parallel_dml 을 hint에 추가 필요

 

- 병렬처리는 작업은 빨리지는데, 자원을 많이 사용함

- 여러개 띄워서 동시 수행해서 cpu, memory를 더 많이 사용

- OLTP같은 온라인 프로그램에 parallel을 쓰면 안됨

- 여러 사람이 쓰는 온라인 프로그램에 parallel이 쓰면 더 많은 자원을 사용해서 시스템이 느려짐 (cpu, memory과다 사용)

- 새벽에 도는 일부 배치 프로그램, DW에만 사용

- CTAS SQL에 parallel을 넣으면, CTAS로 생성된 테이블의 degree속성에 parallel 값이 들어감

   → parallel hint를 안줘도 parallel로 수행됨

 

* degree : table의 병렬 처리방식을 지정

메시지

- 화면에서 사용되는 단어들을 각 화면마다 하드코딩으로 작성했다면, 수정이 필요한 경우 수정할 단어가 포함된 모든 화면을 찾아서 하나하나 수정을 해야 함

- 화면의 개수가 적으면 괜찮지만, 화면이 많으면 유지보수에 어려움이 있음

=> 메시지 : 화면에서 사용되는 다양한 단어(메시지)를 한 곳에서 관리할 수 있도록 기능

 

MessageSource

- 메시지 관리를 위해서 스프링에서  MessageSource를 스프링 빈으로 등록함

- MessageSource은 인터페이스고, 구현체인 ResourceBundleMessageSource를 스프링으로 등록해서 사용하면 됨

=> 스프링 부트는 자동으로 MessageSource를 스프링 빈으로 등록함

public interface MessageSource {  
    @Nullable
    String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

    String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}

 

MessageSource 설정

application.properties

spring.messages.basename=messages

=> 기본값

=> /resources/아래에 messages_en.properties, messages_ko.properties, messages.properties라는 파일을 만들어두면 자동으로 인식하여 메시지 기능에 사용함

=> messages.properties을 기본 값으로 사용

 

messages.properties

hello=안녕
hello.name=안녕 {0}

messages_en.properties

hello=hello
hello.name=hello {0}

=> {0} 은 파라미터임

 

MessageSourceTest

@SpringBootTest
public class MessageSourceTest {

    @Autowired
    MessageSource ms;

    @Test
    void helloMsg() {
        String resultMsg = ms.getMessage("hello", null, Locale.KOREA);
        // String resultMsg = ms.getMessage("hello", null, null);        
        assertThat(resultMsg).isEqualTo("안녕");
    }

    // msg 찾지 못하면 NoSuchMessageException
    @Test
    void notFoundMsgCode() {
        assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
                .isInstanceOf(NoSuchMessageException.class);
    }

// 메시지가 없는 경우 기본 메시지 (기본MSG)
    @Test
    void notFoundMsgCodeDefaultMsg() {
        String resultMsg = ms.getMessage("no_code", null, "기본MSG", null);
        assertThat(resultMsg).isEqualTo("기본MSG");
    }

// 파라미터 사용
    @Test
    void argMsg() {
        String resultMsg = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
        assertThat(resultMsg).isEqualTo("안녕 Spring");
    }

// Locale별로 메시지 가져오기
    @Test
    void defaultMsg() {
        assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
        assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("안녕");
    }

}

=> getMessage() 메서드를 활용하여 messages.properties, messages_en.properties에서 알맞은 메시지를 불러옴

 

메시지 적용해보기

messages.properties

label.item=상품
label.item.id=상품 ID
label.item.itemName=상품명
label.item.price=가격
label.item.quantity=수량
label.item.open=판매여부
label.item.regions=판매지역
label.item.itemType=상품종류
label.item.deliveryCode=배송방식

page.items=상품 목록
page.item=상품 상세
page.addItem=상품 등록
page.updateItem=상품 수정
button.save=저장
button.cancel=취소

 

Thymeleaf로 메시지 불러오기 ( #{...} )

- 메시지 표현식인 #{...}을 사용하면 메시지를 불러올 수 있음

ex)

<h2 th:text="#{page.addItem}">상품 등록</h2>

 

국제화

messages_en.properties

label.item=Item
label.item.id=Item ID
label.item.itemName=Item Name
label.item.price=price
label.item.quantity=quantity
label.item.open=open
label.item.regions=region
label.item.itemType=itemType
label.item.deliveryCode=delivery

page.items=Item List
page.item=Item Detail
page.addItem=Item Add
page.updateItem=Item Update
button.save=Save
button.cancel=Cancel

=> 국제화 적용은 그냥 message_en.properties만 만들어두면 끝난 것임

 

Spring의 국제화 메시지 선택 방법

- Locale정보를 알아야 messages.properties을 적용할지 messages_en.properties을 적용할지 선택 가능

- Spring은 기본적으로는 LocaleResolver인터페이스의 구현체인 AcceptHeaderLocaleResolver를 활용하며, HTTP Reqeust Header의 Accept-Language보고 국제화 메시지를 선택함

  • 한국어 : Accept-Language: ko,en;q=0.9,en-US;q=0.8     => messages.properties 사용
  • 영어 : Accept-Language: en,ko;q=0.9,en-US;q=0.8       => messages_en.properties 사용

 

LocaleResolver

- Spring은 Locale선택 방식을 변경할 수 있는 인터페이스 LocaleResolver를 활용하며, 기본적으로는 Accept-Language를 활용하는 구현체 AcceptHeaderLocaleResolver를 사용함

- LocaleResolver를 구현체를 따로 만들어서 Accept-Language대신 사용자가 직접 언어를 선택하게 하고 쿠기나 세션기반(SessionLocaleResolver)으로 국제화 메시지를 선택하게 할 수도 있음

=> 인터페이스화 되어있는 스프링 장점

 

SessionLocaleResolver

@Bean
public SessionLocaleResolver localeResolver() {
	SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
	sessionLocaleResolver.setDefaultLocale(new Locale("ko"));
	return sessionLocaleResolver;
}

사용자가 언어 선택하면 해당 사용자의 session에 설정

@RequestMapping(value = "/localeChg")
public void changeLocale(String language, HttpSession session) {
	session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale(language));
}

 

 

 

 

https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

 

Tutorial: Thymeleaf + Spring

Preface This tutorial explains how Thymeleaf can be integrated with the Spring Framework, especially (but not only) Spring MVC. Note that Thymeleaf has integrations for both versions 3.x and 4.x of the Spring Framework, provided by two separate libraries c

www.thymeleaf.org

 

Thymeleaf + Spring 기능

  • SpringEL문법 통합
  • 스프링 Bean호출 가능 ex) ${@MyBean.doSomething()}
  • 편리한 <form> 관리를 위한 속성들
    th:object, th:field, th:error, th:errorclass
  • checkbox, radio bution, List 등을 편리하게 사용할 수 있는 기능
  • 스프링 메시지, 국제화 지원
  • 스프링 검증, 오류 처리 지원
  • 스프링 변환 서비스 지원

 

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

=> build.gradle에 추가하면, 라이브러리를 다운로드하고, Thymeleaf관련 Bean을 등록해줌

 

 

입력 <form> 처리

- Thymeleaf가 제공하는 기능을 활용하여 <Form>을 효율적으로 다룰 수 있음

  • th:object : 폼 커맨드 객체, <form>에 적용하여, 해당 폼에서 다룰 객체를 선택함
  • *{...} : 선택 변수 식, th:object에 있는 객체에 접근
  • th:field : id, name, value속성을 자동으로 추가해줌
    <input type="text" th:field="*{itemName}">
    => 렌더링=> value에는 실제 값이 들어감
    <input type="text" id="itemName" name="itemName" value="">​

 

th:object

- th:object를 사용하기 위해서(HTML 렌더링을 위해서)는 Controller로부터 객체를 넘겨받아야 함

- 등록 화면(addForm)을 띄우는 Controller에 model에 빈 Item객체를 넘기는 코드를 추가

 

변경 전

@GetMapping("/add")
public String addForm(Model model) {    
    return "form/addForm";
}

변경 후

@GetMapping("/add")
public String addForm(Model model) {
    model.addAttribute("item", new Item()); // form 렌더링(th:object 를 위해 빈 객체 넘기기)
    return "form/addForm";
}

 

addForm.html 에 th:object작성

<form action="item.html" th:action th:object="${item}" method="post">
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
    </div>

    ......

</form>

=> 결과

<form action="" method="post">
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" class="form-control" placeholder="이름을 입력하세요" name="itemName" value="">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" class="form-control" placeholder="가격을 입력하세요" name="price" value="">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" class="form-control" placeholder="수량을 입력하세요" name="quantity" value="">
    </div>
                
    ......

=> th:object="${item}"으로 해당 <form>에서 사용한 객체를 선택함 (Controller에서 넘긴 빈 Item객체를 선택함)

=> th:field=*{itemName}"을 사용하여 ${item}의 itemName을 적용함 (th:field="${item.itmeName}"과 동일한 내용)

=> th:field=*{itemName}"으로 해당 <input>에 id, name, value속성을 자동으로 추가할 수 있음 => 개발이 간편함

=> value속성에는 실제 Controller에서 넘겨준 Item객체의 값이 들어감 (지금은 빈 객체이므로 value="")

 

=> <form action="item.html" th:action ... >에서 th:action으로 인해 th:action=""으로 렌더링이 되는데, HTML에서 <form>의 action속성 값이 없는 경우, 해당 페이지에 접근한 URL + method속성 값으로 <form> 내용을 제출함

(GET /add으로 addForm을 띄움 => POST /add로 <form>을 제출)

 

 

editForm.html

<form action="item.html" th:action th:object="${item}" method="post">
    <div>
        <label for="id">상품 ID</label>
        <input type="text" id="id" th:field="*{id}" class="form-control" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" th:field="*{itemName}" class="form-control" value="상품A">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" th:field="*{price}" class="form-control" value="10000">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" th:field="*{quantity}" class="form-control" value="10">
    </div>
    
    ......

=> 결과

<form action="" method="post">
    <div>
        <label for="id">상품 ID</label>
        <input type="text" id="id" class="form-control" readonly name="id" value="1">
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" class="form-control" value="itemA" name="itemName">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" class="form-control" value="10000" name="price">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" class="form-control" value="10" name="quantity">
    </div>
    
    ......

=> th:field를 사용하지 않았다면, 개발자가 수정할 데이터를 보여주기 위해 각 <input>마다 value="${item. 필드명}"을 작성해줘야 했었음

=> th:field="*{item}"을 사용하여 자동으로 value속성과 속성 값을 만들 수 있음

=> addForm과 달리 editForm에는 값이 있는 Item객체가 넘어오므로 value속성에 실제 Item객체의 값이 있음

 


 

체크박스, 라디오 버튼, 셀렉트 박스 다루기

  • 판매 여부 : 체크박스
  • 판매지역 : 다중 체크박스 (여러 개 선택 가능)
  • 상품 종류 : 라디오 버튼 (하나만 선택 가능)
  • 배송방식 : 셀렉트 박스 (하나만 선택 가능)

- 체크박스, 라디오 버튼, 셀렉트 박스에서 사용할 데이터들은 Enum, Map, List 등으로 관리할 수 있음

  • Enum : 정적이고, 코드가 배포되어야 변경이 되는 경우
  • Map, List, Java Obect : DB에서 실시간/동적으로 데이터가 변경되는 경우

 

상품 종류 => ENUM => 라디오 버튼

public enum ItemType {

    BOOK("도서"), FOOD("음식"), ETC("기타");

    private final String description;

    ItemType(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

배송방식 => Java Object => 셀렉트 박스

@Data
@AllArgsConstructor
public class DeliveryCode {

    private String code;	// FAST, NORMAL, SLOW
    private String displayName;	// 빠른배송, 일반배송, 느린배송

}

상품 객체 Item

@Data
public class Item {

    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    private Boolean open;           // 판매여부
    private List<String> regions;   // 판매지역
    private ItemType itemType;      // 상품종류
    private String deliveryCode;    // 배송방식

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

 

체크박스 1 - 단일 - 판매 여부

addForm.html

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" name="open" class="form-check-input">
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

=> 이렇게 작성하고 

  1. 체크박스를 선택 O : itemName=AAA&price=234&quantity=34&open=on
    => 체크박스를 선택하면 open이름으로 on이라는 데이터가 넘어가는데, 스프링은 on을 true로 변환해줌
  2. 체크박스를 선택 X : itemName=BBB&price=123&quantity=232
    => 체크박스를 선택하지 않으면, open이라는 필트 자체가 전송되지 않음

=> 문제점 : 체크박스를 선택하지 않은 것도 의미가 있는 것인데(판매중지) 아무런 값이 안 넘어가는 문 게가 있음, 이렇게 되면 체크를 해제하고 저장버튼을 눌러도 체크 해제라는 정보가 저장되지 못함

 

해결방안

- 위 문제 해결을 위해 스프링에서는 히든 필드를 활용함

- 히든 필드는 항상 전송됨

- 기존 필드명에 _만 앞에 붙여서 전송하면 처리해줌

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" name="open" class="form-check-input">
        <input type="hidden" name="_open" value="on"> <!-- 히든 필드 -->
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

=> 히든 필드에 name="_open"이라는 이름으로 작성

  1. 체크박스를 선택 O : itemName=AAA&price=123&quantity=123&open=true&_open=on
    => open=on&_open=on 두 개가 전송됨 => true로 인식
  2. 체크박스를 선택 X : itemName=BBB&price=123&quantity=123&_open=on
    => _open=on 만 전송됨 => false로 인식

 

그런데, 개발자가 매번 input에 히든 필드를 작성하는 것은 귀찮음... => Thmyeleaf기능으로 해결 가능

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" th:field="*{open}" class="form-check-input">        
        <!--<input type="hidden" name="_open" value="on"> --><!-- 히든 필드 th:field="*{open}" or th:field="${item.open}" 가 만들어줌-->
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

=> 결과

<!-- single checkbox -->
<div>판매 여부</div>
<div>
    <div class="form-check">
        <input type="checkbox" id="open" class="form-check-input" name="open" value="true"><input type="hidden" name="_open" value="on"/>
        <!--<input type="hidden" name="_open" value="on"> --><!-- 히든 필드 th:field="*{open}" or th:field="${item.open}" 가 만들어줌-->
        <label for="open" class="form-check-label">판매 오픈</label>
    </div>
</div>

=> <form>의 th:object="${item}"에 접근하는 th:field="*{open}"을 작성

=> <input>에 id, name, value추가

=> <input type="hidden" name="_open" value="on"/> 히든 필드도 자동으로 추가해줌

 

 

 

체크박스 2 - 다중 - 판매 지역

- 체크박스에서 사용할 데이터를 전달

// 지역정보 넘겨주기
@ModelAttribute("regions")
public Map<String, String> regions() {
    Map<String, String> regions = new LinkedHashMap<>();
    regions.put("SEOUL", "서울");
    regions.put("BUSAN", "부산");
    regions.put("JEJU", "제주");    
    return regions;
}

@ModelAttribute

- 위와 같이 @ModelAttribute를 추가해주면, 해당 클래스의 Controller들이 실행될 때마다 작성된 내용을 실행함 (Map생성 후, 지역정보를 담은 후에 model에 Map을 담음 → HTML로 넘어감)

- 공통으로 처리가 가능함

- 하지만, 매번 Controller호출마다 실행되기 때문에, 미리 만들어놓고 가져오는 형태로 리팩토링

 

=> RegionsInit 클래스 (싱글톤 클래스와 매우 유사함)

- static으로 미리 지역정보를 초기화 해두고, @ModelAttribute메서드에서는 Map을 받아오기

public class RegionsInit {

    private static final Map<String, String> regions = new HashMap<>();
    static {
        regions.put("SEOUL", "서울");
        regions.put("BUSAN", "부산");
        regions.put("JEJU", "제주");
    }

    private RegionsInit() {
    }

    public static Map<String, String> getRegions() {
        return regions;
    }
}
@ModelAttribute("regions")
public Map<String, String> regions() {
    Map<String, String> regions = RegionsInit.getRegions();
    return regions;
}

 

addForm

<!-- multi checkbox -->
<div>
    <div>등록 지역</div>
    <div th:each="region : ${regions}" class="form-check form-check-inline">
        <input type="checkbox" th:field="*{regions}" th:value="${region.key}" class="form-check-input">
        <label th:for="${#ids.prev('regions')}" th:text="${region.value}" class="form-check-label">서울</label>
    </div>
</div>

=> 결과

<!-- multi checkbox -->
<div>
    <div>등록 지역</div>
    <div class="form-check form-check-inline">
        <input type="checkbox" value="BUSAN" class="form-check-input" id="regions1" name="regions"><input type="hidden" name="_regions" value="on"/>
        <label for="regions1" class="form-check-label">부산</label>
    </div>
    <div class="form-check form-check-inline">
        <input type="checkbox" value="JEJU" class="form-check-input" id="regions2" name="regions"><input type="hidden" name="_regions" value="on"/>
        <label for="regions2" class="form-check-label">제주</label>
    </div>
    <div class="form-check form-check-inline">
        <input type="checkbox" value="SEOUL" class="form-check-input" id="regions3" name="regions"><input type="hidden" name="_regions" value="on"/>
        <label for="regions3" class="form-check-label">서울</label>
    </div>
</div>

=> th:field="*{regions}"이 id, name속성을 추가해줌

=> 이때, HTML에서 id속성값은 유일해야 하므로 뒤에 번호를 붙여서 만들어줌(regions1, regions2, regions3)

=> th:each="region : ${regions}"에서 region은 Map이므로 value는 th:value="${region.key}"로 생성(SEOUL, BUSAN< JEJU)하고, <label>에는 th:text="${item.value}"로 생성함(서울, 부산, 제주)

=> <label>은 <input>의 id를 알아야 함 : th:for="${#ids.prev('regions')}"을 통해 <input>의 id를 알아올 수 있음

=> 체크박스 이기 때문에, th:field에 의해 히든 필드도 생성됨 <input type="hidden" name="_regions" value="on"/>

 

ex) 부산, 제주 선택 : itemName=AAA&price=123&quantity=123&open=true&_open=on&regions=BUSAN&_regions=on&regions=JEJU&_regions=on&_regions=on

'WEB > spring' 카테고리의 다른 글

[Spring] 메시지, 국제화  (0) 2022.01.23
[Spring] Thymeleaf 기본 기능 정리  (0) 2022.01.16
[Spring] Redirect vs Forward  (0) 2022.01.02
[Spring] WEB-INF 디렉토리  (0) 2022.01.02
[Spring] Spring Bean Scope, Provider, 프록시  (0) 2021.12.30

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

 

Tutorial: Using Thymeleaf

1 Introducing Thymeleaf 1.1 What is Thymeleaf? Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text. The main goal of Thymeleaf is to provide a

www.thymeleaf.org

 

Thymeleaf 특징

- 서버 사이드 HTML 렌더링 : 백엔드 서버에서 HTML을 동적으로 렌더링

- 내추럴 템플릿 : 순수 HTML을 최대한 유지하면서 뷰 템플릿의 역할도 할 수 있음, Thymeleaf로 작성한 HTML 파일은 그냥 브라우저로 열어도 내용을 확인할 수 있음(JSP는 불가능), 서버의 렌더링을 통해서 동적으로 변경된 결과물을 얻을 수도 있음

- 스프링 지원 : 스프링과 잘 연동됨, 스프링과 연동된 편리한 기능들 보유

 

Thymeleaf 선언

<html xmlns:th="http://www.thymeleaf.org">

- Thymeleaf는 기본적으로 HTML의 태그에 속성으로 들어가서 동작함

 

텍스트 

- th:text, th:utext, [[${data}]], [(${data})]

- HTML의 콘텐츠 영역에 데이터를 출력

model.addAttribute("data", "Hello!!");
<p th:text="${data}"></p>

<li>[[${data}]]</li>

=> model에 들어있는 data를 가져와 출력하기

 

Escape

- HTML 문서는 <, > 같은 특수문자로 이루어져 있음

model.addAttribute("data", "Hello <b>Spring!</b>");
<li th:text="${data}"></li>
 <li>Hello &lt;b&gt;Spring!&lt;/b&gt;</li>

-  Hello Spring! 를 원했는데,  브라우저에는 Hello <b>Spring!</b> 로 나옴

- <, > 가 escape처리가 되어서 &lt;,  &gt; 로 바뀌어서 <b>의 역할(굵은 글씨)을 수행 못함

- th:text, [[...]] 은 기본적으로 escape를 제공함

 

HTML 엔티티

- 브라우저는 <를 HTML 태그의 시작으로 인식함, <를 태그의 시작이 아닌 그냥 문자로 인식시키려면 unescape가 필요함( th:utext, [(${data})] )

<li th:utext="${data}"></li>
<li>[(${data})]</li>

=> 기본적으로 escape를 사용하고, 필요한 경우에만 unescape를 사용하자

 

SpringEL

- 변수 표현식 : ${...}

@GetMapping("/variable")
public String variable(Model model) {
    User userA = new User("userA", 22);
    User userB = new User("userB", 26);

    List<User> list = new ArrayList<>();
    list.add(userA);
    list.add(userB);

    Map<String, User> map = new HashMap<>();
    map.put("userA", userA);
    map.put("userB", userB);

    model.addAttribute("user", userA);
    model.addAttribute("users", list);
    model.addAttribute("userMap", map);
    return "basic/variable";
}

@Data
static class User {
    private String username;
    private int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
<ul>Object
  <li>${user.username} = <span th:text="${user.username}"></span></li>
  <li>${user['username']} = <span th:text="${user['username']}"></span></li>
  <li>${user.getUsername()} = <span th:text="${user.getUsername()}"></span></li>
</ul>
<ul>List
  <li>${users[0].username} = <span th:text="${users[0].username}"></span></li>
  <li>${users[0]['username']} = <span th:text="${users[0]['username']}"></span></li>
  <li>${users[0].getUsername()} = <span th:text="${users[0].getUsername()}"></span></li>
</ul>
<ul>Map
  <li>${userMap['userA'].username} = <span th:text="${userMap['userA'].username}"></span></li>
  <li>${userMap['userA']['username']} = <span th:text="${userMap['userA']['username']}"></span></li>
  <li>${userMap['userA'].getUsername()} = <span th:text="${userMap['userA'].getUsername()}"></span></li>
</ul>

 

지역변수 : th:with

<div th:with="first=${users[0]}">
  <p>첫번째 사람 : <span th:text="${first.username}"></span></p>
</div>

=> users중 첫 번째 User객체를 first로 지정해서 사용

 

 

기본 객체

${#request} : HttpServletRequest

${#response}

${#session}

${#servletContext}

${#locale}

 

- 클라이언트 요청 파라미터는 따로 Controller에서 다시 model로 넘길 필요 없이 param을 사용 하서 간편하게 접근이 가능함 ( ${param.파라미터명} )

- 요청 : http://localhost:8080/basic/basic-objects?paramData=Hello

<span th:text="${param.paramData}"></span>
@GetMapping("/basic-objects")
public String basicObject(HttpSession session) {
    session.setAttribute("sessionData", "Hello Session");
    return "basic/basic-objects";
}

@Component("helloBean")
static class Hello {
    public String hello(String data) {
        return "Hello" + data;
    }
}
<li>Request Parameter = <span th:text="${param.paramData}"></span></li>
<li>session = <span th:text="${session.sessionData}"></span></li>
<li>spring bean = <span th:text="${@helloBean.hello('Spring!')}"></span></li>

=> ${param.~~} : 요청 파라미터 접근

=> ${session.~~} : HTTP 세션 접근

=> ${@helloBean.hello('Spring!')} : 스프링 빈 접근

 

URL 링크

- URL 링크 생성 : @{...}

@GetMapping("/link")
public String link(Model model) {
    model.addAttribute("param1", "data1");
    model.addAttribute("param2", "data2");
    return "basic/link";
}
<ul>
    <li><a th:href="@{/hello}">basic url</a></li>
    <li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello query param</a></li>
    <li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
    <li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>
</ul>

=> 결과

<ul>
    <li><a href="/hello">basic url</a></li>
    <li><a href="/hello?param1=data1&amp;param2=data2">hello query param</a></li>
    <li><a href="/hello/data1/data2">path variable</a></li>
    <li><a href="/hello/data1?param2=data2">path variable + query parameter</a></li>
</ul>

1) 단순 URL

2) 쿼리 파라미터 생성 : (..)에 있는 부분은 쿼리 파라미터로 생성됨

3) PathVariable 생성 : URL 경로상에 변수가 있으면 (..)에 있는 부분은 PathVariable로 생성됨

4) 쿼리 파라미터 + PathVariable 생성

 

리터럴

- 리터럴은 소스 코드상에 고정된 값

- 문자 리터럴은 항상 ' 로 감싸야함

<span th:text="'hello'"></span>

- 항상 ' 로 감싸는 건 귀찮으므로, 공백 없이 이어진다면 '를 생략 가능

<span th:text="hello"></span>

- 공백이 있으면 '를 작성해야 함

<span th:text="'hello world!'"></span>

 

리터럴 대체

- |...| 

<span th:text="'hello ' + ${data}"> <!-- 이거 대신 -->
<span th:text="|hello ${data}|"></span> <!-- 리터럴 대체 사용하는 것이 간편 -->

 

속성 값 설정(대체)

- th:name, th:classappend, th:checked="false"

- Thymeleaf는 보통 HTML 태그에 th:*라는 속성을 지정해서 동작하는데, th:*를 작성하면 기존의 속성을 대체해 버림

<body>
    <h1>속성 설정</h1>
    <input type="text" name="mock" th:name="userA" />
    <h1>속성 추가</h1>
    - th:attrappend = <input type="text" class="text" th:attrappend="class=' large'" /><br/>
    - th:attrprepend = <input type="text" class="text" th:attrprepend="class='large '" /><br/>
    - th:classappend = <input type="text" class="text" th:classappend="large" /><br/>
    <h1>checked 처리</h1>
    - checked o <input type="checkbox" name="active" th:checked="true" /><br/>
    - checked x <input type="checkbox" name="active" th:checked="false" /><br/>
    - checked=false <input type="checkbox" name="active" checked="false" /><br/>
</body>

=> 결과

<body>
    <h1>속성 설정</h1>
    <input type="text" name="userA" />
    <h1>속성 추가</h1>
    - th:attrappend = <input type="text" class="text large" /><br/>
    - th:attrprepend = <input type="text" class="large text" /><br/>
    - th:classappend = <input type="text" class="text large" /><br/>
    <h1>checked 처리</h1>
    - checked o <input type="checkbox" name="active" checked="checked" /><br/>
    - checked x <input type="checkbox" name="active" /><br/>
    - checked=false <input type="checkbox" name="active" checked="false" /><br/>
</body>

=> th:name="userA"가 기존의 name="mock"을 대체

=> large라는 클래스 속성 값을 추가, attrappend, attrprepend는 앞뒤로 띄어쓰기를 작성해줘야 함, classappend는 알아서 붙임

=> th:checked="false"로 checked 속성을 제거함 (HTML <input type="checkbox>는 checked라는 속성만 있으면 true/false 관계없이 checked 되어있음)

 

반복

- th:each

@GetMapping("/each")
public String each(Model model) {
    addUser(model);
    return "basic/each";
}

private void addUser(Model model) {
    ArrayList<User> list = new ArrayList<>();
    list.add(new User("aaa", 12));
    list.add(new User("bbb", 22));
    list.add(new User("ccc", 32));
    model.addAttribute("users", list);
}
<body>
<h1>기본 테이블</h1>
<table border="1">
    <tr>
        <th>username</th>
        <th>age</th>
    </tr>
    <tr th:each="user : ${users}">
        <td th:text="${user.username}">username</td>
        <td th:text="${user.age}">0</td>
    </tr>
</table>

=> ${users}의 값을 하나씩 꺼내서 user에 담는 동작을 반복

<table border="1">
    <tr>
        <th>count</th>
        <th>username</th>
        <th>age</th>
        <th>etc</th>
    </tr>
    <tr th:each="user, userStat : ${users}">
        <td th:text="${userStat.count}">username</td>
        <td th:text="${user.username}">username</td>
        <td th:text="${user.age}">0</td>
        <td>
            index = <span th:text="${userStat.index}"></span>
            count = <span th:text="${userStat.count}"></span>
            size = <span th:text="${userStat.size}"></span>
            even? = <span th:text="${userStat.even}"></span>
            odd? = <span th:text="${userStat.odd}"></span>
            first? = <span th:text="${userStat.first}"></span>
            last? = <span th:text="${userStat.last}"></span>
            current = <span th:text="${userStat.current}"></span>
        </td>
    </tr>
</table>

=> 결과

=> userStat이라는 파라미터를 통해 반복의 상태를 확인할 수 있음, 생략도 가능하며 생략하는 경우 변수명+ Stat으로 사용하면 됨

  • index : 0부터 시작하는 값
  • count : 1부터 시작하는 값
  • size : 전체 사이즈
  • even, odd : 홀수, 짝수 여부( boolean )  (count 기준으로)
  • first, last : 처음, 마지막 여부( boolean )
  • current : 현재 객체

 

조건부 평가

- Thymeleaf의 조건식 : th:if, th:unless, th:switch + th:case

@GetMapping("/condition")
public String condition(Model model) {
    addUser(model);
    return "basic/condition";
}

private void addUser(Model model) {
    ArrayList<User> list = new ArrayList<>();
    list.add(new User("aaa", 12));
    list.add(new User("bbb", 22));
    list.add(new User("ccc", 32));
    model.addAttribute("users", list);
}
<tr th:each="user : ${users}">    
    <td>
        <span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
        <span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
    </td>
</tr>

=> 조건에 맞지 않으면 아예 해당 태그는 무효화됨

<tr th:each="user, userStat : ${users}">
    <td th:text="${userStat.count}">1</td>
    <td th:text="${user.username}">username</td>
    <td th:switch="${user.age}">
        <span th:case="10">10살</span>
        <span th:case="20">20살</span>
        <span th:case="*">기타</span>
    </td>
</tr>

=> switch-case로 선택적으로 렌더링 가능, * 는 java의 default와 동일

 

 

블록

th:block

- HTML 태그의 속성에 사용되지 않는 Thymeleaf 자체 태그

- 일반적인 반복이 어려운 경우 사용

<th:block th:each="user : ${users}">
    <div>
        사용자 이름1 <span th:text="${user.username}"></span>
        사용자 나이1 <span th:text="${user.age}"></span>
    </div>
    <div>
        요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span>
    </div>
</th:block>

 

자바스크립트 인라인

- <script th:inline="javascript"> : Javascript에 Thymeleaf를 적용하여 넘어온 값을 편리하게 사용할 수 있도록 도움

<script th:inline="javascript">
@GetMapping("/javascript")
public String javascript(Model model) {
    model.addAttribute("user", new User("userA", 23));
    addUser(model);
    return "basic/javascript";
}

private void addUser(Model model) {
    ArrayList<User> list = new ArrayList<>();
    list.add(new User("aaa", 12));
    list.add(new User("bbb", 22));
    list.add(new User("ccc", 32));
    model.addAttribute("users", list);
}
<script>
   var username = [[${user.username}]];
   var age = [[${user.age}]];
   //자바스크립트 내추럴 템플릿
   var username2 = /*[[${user.username}]]*/ "test username";
   //객체
   var user = [[${user}]];
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
   var username = [[${user.username}]];
   var age = [[${user.age}]];
   //자바스크립트 내추럴 템플릿
   var username2 = /*[[${user.username}]]*/ "test username";
   //객체
   var user = [[${user}]];
</script>

=> 결과 소스

<!-- 자바스크립트 인라인 사용 전 -->
<script>
     var username = userA;
     var age = 23;
     //자바스크립트 내추럴 템플릿
     var username2 = /*userA*/ "test username";
     //객체
     var user = BasicController.User(username=userA, age=23);
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script>
     var username = "userA";
     var age = 23;
     //자바스크립트 내추럴 템플릿
     var username2 = "userA";
     //객체
     var user = {"username":"userA","age":23};
</script>

=> 사용 전 : userA에 " 가 없어서 오류가 발생함 > var username = "[[${user.username}]]"; 직접 " 로 감싸줘야 하는 번거로움

=> 사용 후 : Thymeleaf가 알아서 문자 타입으로 인식하여 " 를 포함시켜 줌, javascript에서 문제가 될 수 있는 문자는 escape 처리도 해줌

 

model에는 user라는 이름으로 User객체가 담겨있는데, 

=> 사용 전 : 그냥 toString으로 생성됨 ( var user = BasicController.User(username=userA, age=23); )

=> 사용 후 : 객체를 json형태로 바꿔줌 ( var user = {"username":"userA","age":23}; )

 

자바스크립트 인라인 each

<script th:inline="javascript">
 [# th:each="user, stat : ${users}"]
 var user[[${stat.count}]] = [[${user}]];
 [/]
</script>

=> 결과

<!-- 자바스크립트 인라인 each -->
<script>
 var user1 = {"username":"aaa","age":12};
 var user2 = {"username":"bbb","age":22};
 var user3 = {"username":"ccc","age":32};
</script>

 

 

템플릿 조각

- th:fragment, th:insert, th:replae

- 웹 페이지들에는 서로 동일한 공통 영역이 많음(상단바, 좌측 nav 등등)

@GetMapping("/fragment")
public String fragment() {
    return "template/fragment/fragmentMain";
}

footer.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
    푸터 자리 입니다. --------------------------------------
</footer>
<footer th:fragment="copyParam (param1, param2)">
    <p>파라미터 자리 입니다. --------------------------------</p>
    <p th:text="${param1}"></p>
    <p th:text="${param2}"></p>
</footer>
</body>
</html>

fragmentMain.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>부분 포함</h1>
    <h2>부분 포함 insert</h2>
    <div th:insert="~{template/fragment/footer :: copy}"></div>
    <h2>부분 포함 replace</h2>
    <div th:replace="~{template/fragment/footer :: copy}"></div>
    <h2>부분 포함 단순 표현식</h2>
    <div th:replace="template/fragment/footer :: copy"></div>
    <h1>파라미터 사용</h1>
    <div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
</body>
</html>

=> Controller에서 return "template/fragment/fragmentMain"; 을 해서 fragmentMain.html이 생성될 때 footer.html에 템플릿 조각으로 지정된 ( <footer th:fragment="copy"> ~ </footer> )가 fragmentMain.html의 th:insert, th:replace로 들어옴

=> ~{template/fragment/footer :: copy} 의미 :  footer.html의 th:fragment="copy"라는 템플릿 조각을 가져옴

  • th:insert는 해당 태그 안에 템플릿 조각을 삽입
  • th:replace는 해당 태그를 템플릿 조각으로 대체

=> 이때, fragmentMain의 th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')} 으로

footer의 <footer th:fragment="copyParam (param1, param2)">로 데이터를 넘길 수 있음

 

 

템플릿 레이아웃 1

- 템플릿 조각에 해당 페이지가 원하는 정보를 넣어서 사용하기

ex) <head>를 공통으로 하여 템플릿 조각(base.html)으로 가져오는데, 특정 페이지(layoutMain.html)에서는 <head>에 필요한 것을 추가

@GetMapping("/layout")
public String layout() {
    return "template/layout/layoutMain";
}

base.html

<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header(title,links)">
  <!-- 교체 -->
  <title th:replace="${title}">레이아웃 타이틀</title>
  <!-- 공통 -->
  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
  <link rel="shortcut icon" th:href="@{/images/favicon.ico}">
  <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>
 <!-- 추가 -->
 <th:block th:replace="${links}" />
</head>

layoutMain.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
    <title>메인 타이틀</title>
    <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
<body>
메인 컨텐츠
</body>
</html>

=> 결과

<!DOCTYPE html>
<html>
<head>
  <!-- 교체 -->
  <title>메인 타이틀</title>
  <!-- 공통 -->
  <link rel="stylesheet" type="text/css" media="all" href="/css/awesomeapp.css">
  <link rel="shortcut icon" href="/images/favicon.ico">
  <script type="text/javascript" src="/sh/scripts/codebase.js"></script>
 <!-- 추가 -->
 <link rel="stylesheet" href="/css/bootstrap.min.css">
 <link rel="stylesheet" href="/themes/smoothness/jquery-ui.css">
</head>
<body>
메인 컨텐츠
</body>
</html>

=> layoutMain에서는 <head th:fragment="common_header(title,links)"> ~ </head>를 th:replace로 가져옴

=> 이때 common_header(~{::title}, {~::link})를 사용해서 layoutMain의 <title>과 <link> 2개를 base의 th:replace에 로 보내서 대체시킴

 

템플릿 레이아웃 2

- <html>에 th:fragment을 적용하면 전체 문서를 대체해버릴 수도 있음

@GetMapping("/layoutExtend")
public String layoutExtend() {
    return "template/layoutExtend/layoutExtendMain";
}

layoutFile.html

<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:replace="${title}">레이아웃 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}">
    <p>레이아웃 컨텐츠</p>
</div>
<footer>
    레이아웃 푸터
</footer>
</body>
</html>

layoutExtendMain.html

<!DOCTYPE html>
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title},~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>메인 페이지 타이틀</title>
</head>
<body>
<section>
    <p>메인 페이지 컨텐츠</p>
    <div>메인 페이지 포함 내용</div>
</section>
</body>
</html>

=> layoutExtendMain의 <html>을 layoutFile의 <hrml>로 대체 => 전체 문서가 layoutFile로 바뀌어 버림

=> 대신 layoutExtendMain의 :: layout(~{::title},~{::section})를 사용하여 <title>과 <section>을 layoutFile의 th:replace로 보내서 대체시킴

=> 전체 문서의 틀을 받아오지만, 필요한 부분은 넘겨서 수정이 가능함

Redirect

- 리다이렉트는 클라이언트에 응답 메시지가 전송된 후에, 클라이언트 쪽에서 다시 지정된 경로로 redirect요청을 함

- 아예 새로운 경로로 요청하기 때문에 URL도 바뀜

 

Forward

- 포워드는 클라이언트가 아니라 서버 내부에서 호출하는 것으로 클라이언트는 포워드 여부를 알지 못함

String viewPath = "경로";
RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request, response);

- WEB-INF에 들어있는 jsp파일은 외부(브라우저)에서 직접 호출할 수가 없음(보안성)

- Controller를 통해서만 호출이 가능 + 비즈니스 로직 실행뒤에 호출되기를 바라는 화면임

 

- 브라우저에서 직접 호출하면 404 에러 발생

 

- 그냥 webapp아래에 있는 파일은 바로 접근이 가능함

Bean Scope

- Bean이 존재할 수 있는 범위

- @Scope 어노테이션으로 지정 가능

 

Spring Bean 종류

- 싱글톤 : 스프링은 기본적으로 싱글톤 스코프로 관리됨, 스프링 컨테이너의 시작~종료까지 함께 함

- 프로토타입 : 스프링 컨테이너는 프로토타입 빈 생성과 의존관계 주입까지만 관여, 이후에는 관여하지 않고 해당 빈은 클라이언트가 관리해야 함

- : 웹 환경에서 사용되는 스코프, request, session, application, websocket

 

프로토타입 스코프

- @Scope("prototype")

- 싱글톤 스코프 빈은 스프링 컨테이너에 의해 단 한 개의 빈으로만 관리되지만, 프로토타입 스코프의 빈을 조회하면 스프링 컨테이너는 항상 새로운 빈을 만들어서 전달해줌 (매번 사용할 때마다 항상 새로 만들어서 반환)

- 스프링 컨테이너는 요청이 오면 프로토타입 스코프 빈을 생성, 의존관계 주입, 초기화 후에는 관리를 하지 않음, 그러므로 @PreDestory 같은 종료 메서드는 호출되지 않음

public class ProtoTypeTest {

    @Test
    void protoTypeBean() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ProtoTypeBean.class);
        ProtoTypeBean protoTypeBean1 = ac.getBean(ProtoTypeBean.class);
        ProtoTypeBean protoTypeBean2 = ac.getBean(ProtoTypeBean.class);

        System.out.println("protoTypeBean1 = " + protoTypeBean1);
        System.out.println("protoTypeBean2 = " + protoTypeBean2);

        assertThat(protoTypeBean1).isNotSameAs(protoTypeBean2);
        ac.close();
    }

    @Scope("prototype")
    static class ProtoTypeBean {
        @PostConstruct
        public void init() {
            System.out.println("ProtoTypeBean.init");
        }
        @PreDestroy
        public void destroy() { // 호출 안됨 (prototype 빈 조회시에 생성되고 컨테이너에서 관리X)
            System.out.println("ProtoTypeBean.destroy");
        }
    }

}
ProtoTypeBean.init
ProtoTypeBean.init
protoTypeBean1 = hello.core.scope.ProtoTypeTest$ProtoTypeBean@30e868be
protoTypeBean2 = hello.core.scope.ProtoTypeTest$ProtoTypeBean@66c92293

=> 서로 다른 빈이 생성됨, destroy() 호출 안됨

 

싱글톤 빈에서 프로토타입 빈을 사용하는 경우 가능한 문제점

- 프로토타입 빈의 특징은 매 요청마다 항상 새로 만들어진다는 것임

- 프로토타입 빈이 싱글톤 빈에서 사용되는 경우 이 특징대로 동작하지 않을 수 있음

public class SingleTonWithPrototypeTest1 {   

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientSingleTonBean.class, PrototypeBean.class);

        ClientSingleTonBean clientSingleTonBean = ac.getBean(ClientSingleTonBean.class);
        int count1 = clientSingleTonBean.logic();
        assertThat(count1).isEqualTo(1);

        int count2 = clientSingleTonBean.logic();
        assertThat(count2).isEqualTo(2);
        
        // => 동일한 prototype 빈을 사용하고 있음
    }

    // 싱글톤 빈에 프로토타입 빈을 주입하는 방법
    @Scope("singleton")
    static class ClientSingleTonBean {
        private final PrototypeBean prototypeBean;  // singleton 빈 안에있는 prototype 빈
        
        @Autowired  // prototype 빈을 주입받음, 이때 컨테이너가 prototype 빈을 생성해서 주입해줌
        public ClientSingleTonBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }
        
        public int logic() {
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }

    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }

}

- 싱글톤 빈 ClientSingleTonBean에서 PrototypeBean을 주입받아 사용하게 됨

- singleTonClientUsePrototype() 메서드에서 logic()을 두 번 사용하는데, 원하는 동작은 logic() 호출시마다 새로운 프로토타입 빈이 생성되길 원했지만, 그렇게 동작하지 않음 (count2 값이 2 = 기존 프로토타입 빈의 count값을 증가시켰기 때문)

=> 스프링이 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 되는데, 싱글톤 빈은 생성시점에만 의존관계 주입을 받고 변경되지 않기 때문에, 프로토타입 빈이 새로 생성되기는 하지만 싱글톤 빈과 함께 유지되는 문제점이 있음

 

사용할 때마다 항상 새로운 프로토타입 빈을 생성하는 방법

- Dependency Lookup : 의존관계를 주입받지 않고, 직접 필요한 의존관계를 찾는 것 (싱글톤 빈에서 필요한 프로토타입 빈을 직접 찾아서 받음)

- Provider를 사용하여 구현 가능

ObjectProvider

@Scope("singleton")
static class ClientSingleTonBean {
	@AutoWired
    private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

- ObjectProvider.getObject()를 호출하면 스프링 컨테이너를 통해 해당 프로토타입 빈을 찾아서 받아옴

 

Provider

@Scope("singleton")
static class ClientSingleTonBean {
    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.get();
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

- Provider는 자바 표준임, 스프링에 의존적이지 않음

- javax.inject:javax.inject:1 라이브러리 필요

- Provider.get()을 호출하면 스프링 컨테이너를 통해 해당 프로토타입 빈을 찾아서 받아옴

 

웹 스코프

- 웹 스코프는 웹 환경에서만 동작

- 해당 스코프의 시작~종료까지 컨테이너에서 관리됨, 종료 메서드가 호출됨

  • request : http 요청하나 가 들어와서 나갈 때까지 유지, 각 http요청마다 서로 다른 빈이 생성되고 관리됨
  • session : http session과 동일한 생명주기
  • application : ServletContext와 동일한 생명주기
  • websocket : 웹 소켓과 동일한 생명주기
@Component
@Scope(value = "request")
public class MyLogger {

    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String msg) {
        System.out.println("["+uuid+"]"+"["+requestURL+"]"+msg);
    }

    @PostConstruct
    public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println("["+uuid+"] request scope bean create" + this);
    }

    @PreDestroy
    public void destroy() {
        System.out.println("["+uuid+"] request scope bean close" + this);
    }
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        System.out.println("myLogger = " + myLogger);

        myLogger.setRequestURL(requestURL);
        myLogger.log("LogDemoController Test");

        logDemoService.logic("testId");

        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

    public void logic(String testId) {        
        myLogger.log("LogDemoService id = " + testId);
    }
}

- 싱글톤 빈인 LogDemoController, LogDemoService에 웹 스코프 빈인 MyLogger를 주입받으려 함 : 오류남

=> 오류 이유 : request 스코프 빈은 실제 http요청이 들어와야 생기는 빈으로, LogDemoControllㄹer, LogDemoServicer는 아직 생기지도 않은 MyLogger를 주입받을 수 없음

=> Provider로 해결

 

ObjectProvider, Provider

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    //private final ObjectProvider<MyLogger> myLoggerObjectProvider;
    private final OProider<MyLogger> myLoggerProvider;
    private final LogDemoService logDemoService;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        
        //MyLogger myLogger = myLoggerObjectProvider.getObject();
        MyLogger myLogger = myLoggerProvider.get();

        myLogger.setRequestURL(requestURL);
        myLogger.log("LogDemoController Test");

        logDemoService.logic("testId");

        return "OK";
    }

}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    //private final ObjectProvider<MyLogger> myLoggerObjectProvider;
    private final Provider<MyLogger> myLoggerProvider;

    public void logic(String testId) {
        //MyLogger myLogger = myLoggerObjectProvider.getObject();
        MyLogger myLogger = myLoggerProvider.get();
        myLogger.log("LogDemoService id = " + testId);
    }
}

- ObjectProvider, Provider를 사용해서, 실제로 필요할 때(getObject() 호출/get() 호출)까지 주입을 받지 않음

 

프록시

- MyLogger의 가짜 프록시 클래스를 만들어두고, http요청과 상관없이 MyLogger를 상속받는 가짜 프록시 클래스를 다른 빈에 주입함

@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) // MyLogger의 가짜 프록시 클래스를 만들어 두고 HTTP request와 상관없이 가짜 프록시 클래스를 미리 주입해둠
public class MyLogger {

    ...
}

 

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final MyLogger myLogger;    // 가짜 MyLogger 프록시 클래스를 미리 주입받음
    
    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        System.out.println("myLogger = " + myLogger);   // myLogger = hello.core.common.MyLogger@6634cbd4   // 가짜 MyLogger 프록시 클래스

        myLogger.setRequestURL(requestURL);
        myLogger.log("LogDemoController Test");

        logDemoService.logic("testId");

        return "OK";    
    }

}
@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;    // 가짜 MyLogger 프록시 클래스를 미리 주입받음    

    public void logic(String testId) {        
        myLogger.log("LogDemoService id = " + testId);
    }
}

- @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)를 사용하면 스프링 컨테이너는 CGLIB 바이트코드 조작 라이브러리로 MyLogger가짜 프록시 클래스를 생성해서 빈 등록함

- LogDemoController, LogDemoService에 MyLogger의 가짜 프록시 클래스가 주입됨

- Client에서 실제로 사용하는 요청이 오면 그때 내부에서 진짜 MyLogger빈을 찾아서 위임함

 

 

=> Provider, 프록시의 핵심은 진짜 빈 조회를 실제로 필요한 시점까지 지연할 수 있다는 점

=> 아주 간단한 어노테이션 설정으로 가짜 프록시 객체를 만들 수 있는 것이 다형성, DI컨테이너의 큰 장점

 

 

- RedirectAttribute

1. URL인코딩 

2. pathVariable 처리

3. 쿼리 파라미터 처리

 

- redirect 시킬 때 데이터를 전달할 수 있도록 해줌

- RedirectAttributes를 활용하여 redirect 되는 화면에서 사용자의 행위에 대한 alert 등의 안내를 해줄 수 있음

ex : 상품등록 > 상품 상세화면 redirect >  "상품등록성공!"

 

@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
	Item savedItem = itemRepository.save(item);
	redirectAttributes.addAttribute("itemId", savedItem.getId());
	redirectAttributes.addAttribute("status", true);
	return "redirect:/basic/items/{itemId}";
}

=> 등록 후 /basic/items/{itemId} 로 redirect (itemId, status라는 이름으로 데이터 전달)

=> /basic/items/3?status=true

 

@GetMapping("/{itemId}")
public String item(@PathVariable("itemId") Long id, Model model) {
	Item item = itemRepository.findById(id);
	model.addAttribute("item", item);
	return "basic/item";
}
<h2 th:if="${param.status}" th:text="'상품등록성공!'"></h2>

=> itemId를 활용하여 @PathVariable로 특정 상품 조회 가능

=> status=true를 활용하여 등록 완료 안내 가능

 

- 데이터베이스에 데이터를 저장하는 기법 중 하나, 데이터를 샤드라는 단위로 나눠서 저장 및 처리

- 확장성 문제를 해결하기 위한 방법

- 시간이 지남에 따라 늘어나는 데이터의 양 사용자 수에 대응

- 이전보다 더 많은 데이터를 가지고 더 많은 사용자들이 만들어내는 트랜잭션을 처리하기 위함

- 샤딩은 수평적 파티셔닝과 동일함

- 큰 데이터 테이블을 수평적 파티셔닝을 통해 여러 개의 작은 단위로 분산하여 저장 및 관리

 

 

Database의 샤딩(Sharding)이란? (nesoy.github.io)

 

Database의 샤딩(Sharding)이란?

 

nesoy.github.io

DB분산처리를 위한 sharding | 우아한형제들 기술블로그 (woowahan.com)

 

DB분산처리를 위한 sharding | 우아한형제들 기술블로그

{{item.name}} 소개 저희는 신사업부문에서 Thiiing(띠잉)서비스를 만들고 있는 송재욱/전병두입니다. 이번에는 두 명이 함께 기술블로그를 작성했습니다. 🙂 서비스 오픈전에 아름다운 J곡선 그래프

techblog.woowahan.com

샤딩 - 해시넷 (hash.kr)

 

샤딩 - 해시넷

샤딩(sharding)이란 하나의 거대한 데이터베이스나 네트워크 시스템을 여러 개의 작은 조각으로 나누어 분산 저장하여 관리하는 것을 말한다. 이는 단일의 데이터를 다수의 데이터베이스로 쪼개

wiki.hash.kr