JVM은 Java Virtual Machine의 약자이다. 자바의 대부분 강의에서 초반 부분에서는 System.out.println("Hello world")라는 java 코드를 막 작성하고 이것을 자바 컴파일러로 컴파일하고 콘솔로 실행시키는 실습을 많이 하곤 한다. 이 때 JVM이라는 용어가 등장한다. JVM 자체도 깊게 다루면 끝도 없고 컴퓨터 구조 관련 얘기가 무수히 나오기 때문에 그 땐 이야기를 자세히 다뤄주진 않는다. 이전에 시간을 따로 내서 정리해야 겠다는 생각을 했었고 그게 바로 지금인거 같아서 글을 작성하려 한다. 

 

JVM

자바가 다른 언어와 구별되는 큰 장점으로 가장 많이 거론되는 것이 OS 플랫폼의 독립성이다. WORN(write once, run, anywhere)라는 말이 있는데 이말은 플랫폼 상관없이 한번 쓰기만 하면 어디에서나 동작가능하게 해준다는 말로 이를 가능하게 해주는 것이 바로 JVM이다. (반면 C언어는 OS에 종속적인 컴파일러가 해당 소스코드를 컴파일한다. 그래서 OS 종속적이다. ) 

 

JAVA의 실행

우리가 작성한 자바코드(.java확장자)는 자바 컴파일러가 컴파일하여 자바 바이트코드라는 것이 된다. 이것은 JVM이 실행할 수 있는 형태로 .class 확장자가 붙은 것이 바이트코드이다. JVM이라는 프로그램 자체는 물론 리눅스 전용 JVM, 윈도우 전용 JVM 따로 있지만 .class 파일을 받아서 해석하는 방식은 모두 통일된 방식으로 해석하기 때문에 OS 독립적이라 말한다. JVM이 동작하는 위치를 아래의 그림처럼 도식화 할 수 있다. 

앞에서 언급한 .class는 아직 기계어가 아니다. 아직 중간언어인 셈이다. 이것이 다른 OS에 가더라도 동일하게 동작되는 형태인 것이며 아직 한번의 변환이 더 남았다. 그 과정은 중간언어와 주변 OS 정보들을 정리하여 해당 OS에 적합한 형태로 변환하는 과정이다. 이러한 작업을 하는 것이 Execution Engine이고 이 때 사용되는 방식은 인터프리터 방식으로 이 때 최종 기계어로 바꾸며 한줄 한줄 읽어가면서 실행이 된다. 하지만 같은 메소드라도 반복적으로 호출이 되면 매번 해석하고 수행하는데 있어서 속도가 느리기 때문에 이때 JIT 컴파일러가 사용된다. JIT 컴파일러자주 사용될 수 있는 bytecode를 캐싱하고 최적화함으로써 코드가 interpreter에 의해 여러번 변환되는 작업의 수를 줄일 수 있다. 이와 같이 자바는 컴파일러 방식과 인터프리터 방식을 모두 사용한다. 

 

JVM 구성요소

이제 위 그림에서 구성 요소들을 하나씩 알아보려 한다.

  • Garbage Collector 
    • 메모리 관리 기능을 자동으로 수행한다. 애플리케이션이 생성한 객체의 생존 여부를 판단하여 더이상 사용하지 않는 객체를 메모리 해제한다. ( Garbage Collector에 대한 동작원리는 나중에 정리하자 )
  • Class Loader
    • JVM내로 클래스를 로드(올림)하고 링크(배치)하는 작업을 수행하는 모듈로 런타임시 동적으로 클래스를 로드한다. Runtime Data Area에 올라간다.
  • Execution Engine
    • Class Loader를 통해 JVM 내 런타임 데이터 영역에 배치된 바이트 코드를 실행한다. 자바 바이트 코드를 명령어 단위로 읽어서 실행한다. 
  • Runtime Data Areas
    • JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.
    • 실서버에서 호스팅을 하던 중에  java.lang.OutOfMemoryError: Java heap space라는 에러가 나면 이쪽에서 나는 문제라 한다. ( 해결방법은 구글링~ )
    • 해당 영역은 5가지로 구분된다. Method, Heap, Stack, PC Register, Native Method

Method Area, Heap Area는 모든 스레드가 공유하는 영역,

Stack Area, PC Register, Native Method Stack Area는 각 스레드마다 생성되는 개별영역이다. 

 

공유의 형태를 다음과 같이 도식화할 수 있다. 

(Heap은 모든 스레드가 공유하기 때문에 동기화 문제가 생길 수 있다는 것 주의!)

힙 영역은 가비지 컬렉션에 대상이 되는 공간인데 효율적인 가비지 컬렉션을 수행하기 위해

다음과 같이 여러 영역으로 나뉜다.

 

힙힙!

  • Young Generation : 생명주기가 짧은 객체를 GC 대상으로 한다.
    • Eden : new를 통해 새로 생성된 객체가 위치되고 정기적인 쓰레기를 수집하고나서 살아남으면 Survivor로 이동된다.
    • Survivor1/2 : 살아남은 객체가 순차적으로 이동된다. 
  • Old Generation : 생명 주기가 긴 객체를 GC 대상으로 하는 영역, Young Generation에서 마지막까지 살아남은 객체가 이동
  • Permanent : 클래스와 메소드 메타 정보가 저장된 공간이다. static 변수와 static 메소드가 저장되는 공간
    • java8부터 heap space가 아니다. native area로 이동
    • metaspace라는 용어로 변함
 

마로의 Java(자바) 정리 - 8. 자바 메모리 구조

JVM 구조 실행될 클래스 파일을 메모리에 로드 후 초기화 작업 수행 메소드와 클래스변수들을 해당 메모리 영역애 배치 클래스로드가 끝난 후 JVM은 main 메소드를 찾아 지역변수, 객체변수, 참조

hoonmaro.tistory.com

 

☕ JVM 내부 구조 & 메모리 영역 💯 총정리

저번 포스팅에서는 JRE / JDK / JVM에 대해서 간략하게 알아보는 시간을 가졌다면, 이번 포스팅에서는 JVM의 내부 구조에 대해 좀 더 자세하게 알아보도록 할 예정이다. JVM(자바 가상 머신)은 자바 언

inpa.tistory.com

 

Garbage Collector 제대로 알기

ㅅㅅ슈규슈슉 g..gc 슈슉 g...gc GC! 슈슉

velog.io

 

'자바' 카테고리의 다른 글

스트림 정리  (0) 2023.05.07

최근에 스프링부트 강의를 보는데 이전 버전 Spring Boot에서는 application.yml이 자동 생성되고 특정 버전이후부터는 application.properties 파일이 자동 생성되는 것을 확인할 수 있었다. 이 두가지 파일은 작성하는 설정의 의미는 동일하나 작성 표기법이 살짝다르다. 그리고 서로 변환이 가능해서  properties로 작성하고 싶으면 그 방식대로 작성하고 yml파일 형식으로 작성하고 싶으면 그 방식대로 작성하면 된다. 뭐가 더 편하고 좋다라는 것은 사용하는 사람들 마다 다르다. 개인적으로는 가독성은 properties 파일, 작성의 편의성은 yml파일이 좋다고 생각된다. .properties 파일은 작성법이 직관적이나 yml 파일은 초심자가 이해하는데 약간의 사전지식이 필요하다. 그리고 현재 인프런이나 패스트캠퍼스에 올라온 강의들은 yml형식으로 강의한 것들이 많으므로 이것에 대한 지식은 나름 필요하다고 생각된다. 그래서 이번 포스트에서는 application.properties파일과 application.yml파일의 차이를 알아보고 yml파일 작성법에 대해서 소개하려고 한다.

 

  • 필자는 다음과 같은 프로젝트를 생성하였고 스프링부트 버전은 2.7.13이다. 

프로젝트 생성

  • 해당 버전의 스프링부트 프로젝트에서는 application.properties가 자동생성된다. 이것을 yml 파일로 바꾸고 싶다면 파일의 확장자명만 yml으로 바꾸면 된다. 이 application.properties 파일은 별다른 설정이 없다라도 기본으로 인식이 되지만 새로 생성하는 또다른 설정 파일을 인식하기 위해선 추가로 지정해주는 과정이 필요하다. 

properties 파일 vs yml 파일  

위의 파일은 properties 형식으로 작성한 파일이고 아래 그림은 yml 형식으로 작성한 파일이다. properties 형식은 key, value 형식으로 작성되어 사이사이에 dot(.)을 넣어서 key이름을 작성한다. 사실 이것이 계층구조인데 이러한 계층구조를 properties파일 형식에선 각 계층을 모두 적어준다. 

properties

위에서 dot(.)으로 구분한 key이름은 yml 파일에선 좀 더 심플해진 계층구조로 표현이 되어 상위계층명을 생략하여 다음 라인에 입력한다. 그리고 라인마다 중간 중간에 공백이 있다. 이건 계층의 depth를 포현하며 이때 공백 한칸은 tab을 입력하기 보다는 space 두번을 쳐서 입력하는 것이 좋다. 그리고 key옆의 클론(:)과 value 사이에는 한 칸의 space 있어야 한다. 

yml

그 다음으로는 리스트를 작성할 때의 차이이다. yml 형식의 특징은 반복의 최소화이다. yml 파일의 리스트 표기는 - 로 표기를 한다. 같은 index 표기시엔 다음 라인엔 -를 쓰지 않는다. 반면에 properties 형식에서는 기존 리스트 표기형식과 동일하다. 

리스트 표현형식 : properties           >           yml

주석

  • 주석은 yml 파일, properties 파일 모두 #으로 동일하다.

직접 작성시

  • properties
    • 최대한 인텔리센스에서 제공하는 자동완성으로 작성하는 것이 좋다. 오타를 내는 것을 조심하자.

자동완성

  • yml
    • yml 파일 자동완성은 매우 편하다. 한방에 계층 구조를 만들어준다. 그리고 한라인 한라인 엔터치고 작성하면 작성 속도도 yml 파일이 월등히 빠르다고 생각한다.

자동완성 편해

yml 파일 형식 체크 사이트

 본인이 yml 파일을 잘 작성했는지 확인하려면 다음의 사이트를 확인하면 된다. 

https://jsonformatter.org/yaml-viewer

 

Best YAML Viewer Online

Free YAML Viewer is a web based viewer for YAML

jsonformatter.org

이미 작성된 yml 또는 properties 파일 변환 시 

 직접 수작업으로 변환할 수 있지만 물론 자동으로 변환시켜주는 사이트가 있다. 굉장히 유용하다.

 

 

 JPA에서는 이전에 서블릿이나 스프링에서 사용하던 DAO 클래스와 비슷한 역할을 하는 클래스를 우리가 정의해야 한다.  Jpa의 표준 명세라는 것이 있고 우리가 그것을 가져다 쓰는 방식으로 구현을 하게 되는 것이다. 그래서 간단한 쿼리 같은 것은 메서드 하나로 뚝딱이지만 복잡한 쿼리는 이러한 메서드들로 해결이 되지 않는다는 문제점이 있다. 하지만 JPA 입문 초반에는 복잡한 쿼리에 대한 고민보다는 이러한 표준 명세에 대하여 익히는 것을 우선으로 하되 JpaRepository API 인터페이스 공부를 제대로 잡고 하는 것이 필요하다. 여기서는 '진짜 심플한 예제'부터 시작한다. 해당 엔터티를 하나 정의하고 그에 대한 엔터티 Repository를 만든다. 

 

명명 방식은 다음과 같다.

  • 엔터티명 + Repository (클래스명은 자유이긴한데 일반적으로 이렇게 정의)

이렇게 우리가 정의한 클래스는 JpaRepository를 상속 받아서 써야 한다.

  • 이 때 상속을 받게 되면 기본적으로 부모 클래스에서 이미 정의된 메서드를 사용할 수 있다. 
  • JpaRepository의 클래스 계층구조는 다음과 같다.

요런 계층 구조

  • CrudRepository에서는 existsById, findById, save, saveAll, findAll, count, deleteById, delete, deleteAll 등의 메서드를 정의하고 있다. 말 그대로 Crud에 관련한 기본 메서드 정의이다.
  • PagingAndSortingRepository 
    • 페이징 처리를 위한 메서드를 제공
  • QueryByExampleExecutor 
    • 어떤 명세를 인자로 받으면 그에 맞는 동적으로 쿼리를 제공해준다. 

사용자 정의 메소드

  • 위에서 상속하는 클래스가 제공하는 메서드를 쓰지 않고 JPA가 약속한 규칙에 따라 사용자가 메서드를 정의해서 써야 한다. 주로 목적은 테이블 칼럼에 맞추고 Where절 부분에 해당하는 조건을 파라미터로 받기 위한 것이다. 
  • 메서드 이름은 CamelCase로 입력해야 한다.  
    • 예시)   
      • findBy○○○(String ○○○)
      • findBy○○○Null
      • findAllByOrderBy○○○
      • findAllByOrderBy○○○Desc
      • findBy○○○In(String[] strings)
  • ItemRepository.java
public interface ItemRepository extends JpaRepository<Item, String>{


 // 여기다가 이제 메서드 정의만 추가 ↓↓↓↓↓↓↓
 
 
 
 }
  • m9 
    • where 조건이 붙은 쿼리를 정의하는 느낌으로 구성할 수 있다. 
    • findByName("마우스") ----> ItemRepository에 Item findByName(String name);  추가
      • 아래의 표현은 결과가 동일하다.  
      • Item findByName(String name); 
      • Item findByNameIs(String name);
      • Item findByNameEquals(String name); 
    • itemRepo.findByPrice(100000) ---->  ItemRepository에 Item findByPrice(int price); 추가
@GetMapping("/item/m9")
	public String m9(Model model) {
		//findByName으로 자동으로 구현 
		// Item item = itemRepo.findByName("마우스");
		// Item item = itemRepo.findByPrice(100000);
		// 메서드 이름 패턴이 중요하다. 
		// 가장 중요하게 생각하는 패턴이 > findBy이다. 
		// findBy컬럼명
		// - By : Po
		//Item item = itemRepo.findByNameIs("키보드");
		Item item = itemRepo.findByNameEquals("키보드");
		model.addAttribute("result", item);
		return "item/result";
}
  • m10
    • where절에 And 혹은 Or 사용
    • where절 다중조건
    • itemRepo.findByColorAndOwnerAndPrice("black", "홍길동", 150000);
@GetMapping("/item/m10")
public String m10(Model model) {
    // And, Or
    //Item item = itemRepo.findByColorAndOwner("yellow", "홍길동");
    //Item item = itemRepo.findByColorAndOwnerAndPrice("black", "홍길동", 150000);
    Item item = itemRepo.findByColorOrPrice("gold", 300000);
    //where item0_.color=? and item0_.owner=? and item0_.price=?

    model.addAttribute("result", item);
    return "item/result";
}
  • m11
    • findBy에 정렬 추가 
      • 파라미터 두번째 인자 Sort.by
      • 첫번째 인자로 오름, 내림차순을 정하고, properties를 가변인자로 받아서 정렬 기준을 정한다. 

내용

  • Like 쿼리 : 패턴 매칭
    • findBy○○○Like(패턴)
    • findBy○○○IsLike(패턴)
    • findBy○○○NotLike(패턴)
    • findBy○○○IsNotLike(패턴)
    • 패턴으로 %, _ 등을 넣어서 찾을 수 있다.
  • StartingWith, EndingWith, Containing : ~로 시작거나 끝나거나 포함하는걸로 매칭
    • findBy○○○[Is]StartingWith(검색어)
    • findBy○○○[Is]EndingWith(검색어)
    • findBy○○○[Is]Containing(검색어)
@GetMapping("/item/m11")
	public String m11(Model model) {
		
		//List<Item> list = itemRepo.findByColor("white");
		//List<Item> list= itemRepo.findByColor("white", Sort.by(Sort.Direction.ASC, "price"));
		//color를 조건 price를 기준으로 order by를 했다.
		//List<Item> list= itemRepo.findByColorAndPrice("yellow", 95000);
		//List<Item> list= itemRepo.findByColorOrOwner("white", "홍길동");
		//- findByNameLike()
		//- findByNameIsLike()
		//- findByNameNotLike()  // 이거 빼고 찾아줘 ~ 
		//- findByNameIsNotLike()
				
		//- %, _를 직접 명시 
		//List<Item> list= itemRepo.findByNameLike("키보드");
		//List<Item> list= itemRepo.findByNameLike("%키보드");
		// - findByName[Is]StartingWith
		// - findByName[Is]EndingWith
		// - findByName[Is]Containing
		// List<Item> list= itemRepo.findByNameStartingWith("마우스");
		List<Item> list= itemRepo.findByNameEndingWith("마우스");
		model.addAttribute("list", list);
		return "item/result";
}
  • m12
    • findBy○○○[Is]Null, findBy○○○[Is]NotNull
      • 컬럼값이 null인 레코드 검색
    • findBy○○○[Is]Empty, findBy○○○[Is]NotEmpty
      • 컬럼값이 null이거나 빈문자열인 레코드 검색
    • 정렬 
      • ex) List<Item> list = itemRepo.findAll(Sort.by(Sort.Direction.DESC, "price")); 
      • findAll에 Sort를 받도록 오버로딩 되어 있다.  
      • List<T> findAll(Sort sort);
      • 좀 더 가독성 있는 메서드명 버전
        • List<Item> list = itemRepo.findAllByOrderByColor();
        • List<Item> list = itemRepo.findAllByOrderByColorDesc();
          • 정렬조건 추가
        • List<Item> list = itemRepo.findByOwnerOrderByColorDesc("홍길동");
          • 조건 추가
    • 비교
      • findBy○○○[IS]GreaterThan, [IS]LessThan, [Is]Between, [Is]GreaterThanEqual, [Is]LessThanEqual
        • List<Item> list = itemRepo.findByPriceGreaterThan(100000);
        • List<Item> list = itemRepo.findByPriceGreaterThan(100000, Sort.by("price"));
        • List<Item> list = itemRepo.findByPriceLessThan(100000, Sort.by("price"));
        • List<Item> list = itemRepo.findByPriceBetween(90000, 120000);
        • List<Item> list = itemRepo.findByOrderdateBetween("2023-06-25", "2023-06-27");
    • In, NotIn
      • Where color in ('yellow', 'blue')
      • List 형식으로 인자로 넣는다.
      • findBy○○○In
      • findBy○○○NotIn
List<String> colors = new ArrayList<String>();
colors.add("yellow");
colors.add("blue");		
List<Item> list = itemRepo.findByColorIn(colors);

List<Item> list = itemRepo.findByOwnerNotIn(new String[]{"홍길동", "아무개"});
@GetMapping("/item/m12")
	public String m12(Model model) {
		// [Is]Null, [Is]NotNull
		// - 컬럼값이 null인 레코드 검색 
		
		// [Is]Empty, [Is]NotEmpty
		// - 컬럼값이 null이거나 빈문자열인 레코드 검색
		
		//List<Item> list = itemRepo.findByOwnerNull();
		//List<Item> list = itemRepo.findByOwnerEmpty();
		//List<Item> list = itemRepo.findByOwnerNotNull();
		
		// 정렬 
		// List<Item> list = itemRepo.findAll(Sort.by(Sort.Direction.DESC, "price")); // > findAll
		
		// List<Item> list = itemRepo.findAll(Sort.by("name")); // 오름차순
		
		// List<Item> list = itemRepo.findAllByOrderByColor();
		
		// List<Item> list = itemRepo.findAllByOrderByColorDesc();
		
		// List<Item> list = itemRepo.findByOwnerOrderByColorDesc("홍길동");
		
		//	where item0_.owner=? order by item0_.color desc
		
		
		// [IS]GreaterThan, [IS]LessThan, [Is]Between
		// [Is]GreaterThanEqual, [Is]LessThanEqual
		
		// List<Item> list = itemRepo.findByPriceGreaterThan(100000);
		
		// List<Item> list = itemRepo.findByPriceGreaterThan(100000, Sort.by("price"));
		
		// List<Item> list = itemRepo.findByPriceLessThan(100000, Sort.by("price"));
		
		// List<Item> list = itemRepo.findByPriceBetween(90000, 120000);
		
		// List<Item> list = itemRepo.findByOrderdateBetween("2023-06-25", "2023-06-27");
		
		//IgnoreCase
		//- 특정 컬럼의 대소문자를 구분하지 않고 검색
		// List<Item> list = itemRepo.findByColor("white");
		
		// List<Item> list = itemRepo.findByColorIgnoreCase("White");
		
		
		// In, NotIn
		// - Where color in ('yellow', 'blue')
		
//		List<String> colors = new ArrayList<String>();
//		colors.add("yellow");
//		colors.add("blue");
//		
//		List<Item> list = itemRepo.findByColorIn(colors);
		
		//List<Item> list = itemRepo.findByOwnerIn(new String[]{"홍길동", "아무개"});
		
		List<Item> list = itemRepo.findByOwnerNotIn(new String[]{"홍길동", "아무개"});
		 
		model.addAttribute("list", list);
		
		return "item/result";
	}
  • m13, m14
    • JPA 도메인 클래스 컨버터(Domain Class Converter)
    • PK를 URL Path로 넣거나 쿼리스트링으로 리퀘스트 했을 뿐인데 그걸 베이스로 쿼리를 만들어준다.
    • 컨트롤러 메서드 파라미터 객체에 결과 값이 바로 채워진다. 
    • 코드량이 상당히 줄어든다. 값 조회 용도로만 쓰자
    • 두가지 방식이 있다.
      • RequestParam 방식
      • PathVariable 방식
@GetMapping("/item/m13")
public String m13(Model model, @RequestParam("name") Item result) {

    // PK를 리퀘스트 했을 뿐인데 그걸 베이스로 쿼리를 만들어서 아이템에 넣어주는 작업도 해주는 것이다. 
    // 도메인 클래스 컨버터(Domain Class Converter)
    // - PK를 넘겨서, 바로 Entity를 조회할 수 있다. 

    System.out.println(result);
    model.addAttribute("result", result);
    // item/m13?name=키보드
    //"마우스" > PK  
    // Optional<Item> result = itemRepo.findById(name);
    // model.addAttribute("result", result.get()); // Optional은 꺼내야 한다.

    return "item/result";
}
// 이 떄는 @PathVariable 이나 @RequestParam를 생략하면 안된다. 
@GetMapping("/item/m14/{name}")
public String m14(Model model, @PathVariable("name") Item result) {

    //item/m13?name=마우스
    //item/m13/마우스
    model.addAttribute("result", result);

    return "item/result";
}
  • m15
    • First, Top 등의 최상위 한개, 상위 몇개, 범위 지정등의 쿼리를 정의할 수 있다. 
    • findFirstBy, findTopBy는 레코드 하나를 반환한다.
      • findFirstBy, findTopBy는 결과는 동일
    • findTop숫자By 등의 범위 지정 쿼리도 가능하다.
@GetMapping("/item/m15")
public String m15(Model model) {

    // First
    // Top
    // 둘다 첫번쨰거 가져오는데 First는 무조건하나를 가져오는데 ★ Top은 원하는 범위를 선택한다.
    // Item result = itemRepo.findFirstByOrderByPriceAsc();
    // Item result = itemRepo.findTopByOrderByPriceAsc();
    List<Item> list = itemRepo.findTop3ByOrderByPriceDesc();
    model.addAttribute("list", list);
    return "item/result";
}
  • m16
    • PageRequest : 페이징 기능을 담당한다. 
    • 레코드가 여러개 있을 때 페이징을 하는데 PageRequest.of 메서드로 기능을 처리한다. page로 몇 페이지, size로 몇개씩 가져올 것인가를 지정하고 Sort.by를 넣어서 정렬기준을 정할 수 있다. 
    • 반환값은 PageRequest라는 객체를 반환하며 findPageListBy라는 메서드로 엔터티 형식 객체로 바꿔준다. 
    • List<Item> findPageListBy(PageRequest pageRequest); 이건 ItemRepository에 이렇게 정의가 필요하다. 

밖에서 사용법
of 정의

요청URL : http://localhost:8092/item/m16?page=1
@GetMapping("/item/m16")
	public String m16(Model model, int page) {
		
		// PageRequest 이게 페이징을 담당한다. 
		PageRequest pageRequest = PageRequest.of(page, 5, Sort.by("name"));
		// Page는 0부터다 !!! 
		List<Item> list = itemRepo.findPageListBy(pageRequest);
		model.addAttribute("list", list);
		
		return "item/result";
}
  • m17, m18
    • @Query라는 애너테이션을 사용한다. @Query의 value에 들어가는 부분이 JPQL을 작성할 수 있는데 nativeQuery=true로 지정하면 SQL을 직접 정의할 수 있다. 이렇게 셋팅하면 JPQL과는 성격이 다름
    • @Query(value="select * from Item where color = :color", nativeQuery=true)
    • : 을 써서 파라미터 바인딩이 가능한데 :#을 쓰는 경우도 있다.
@GetMapping("/item/m17")
	public String m17(Model model) {
		
		//@Query 
		// - 사용자 쿼리 작성
		// - 쿼리 메소드 키워드로 작성 불가능 쿼리 > 직접 SQL 작성 
		// select * from Item 
		List<Item> list = itemRepo.findAllItem(); // 메서드 이름이 중요하지 않다. 쿼리를 내맘대로 짠다. 쿼리를 내가 만든다. 
		model.addAttribute("list", list);
		return "item/result";
}

@GetMapping("/item/m18")
	public String m18(Model model, String color) {
		
		List<Item> list = itemRepo.findAllItemByColor(color);
		model.addAttribute("list",list);
		return "item/result";
}
  • ItemRepository.java 부분
// 이렇게 붙여주면 된다. 쿼리가 길고 복잡하면 이방법으로 쓴다. 
@Query(value="select * from Item", nativeQuery=true)
List<Item> findAllItem();

// 이 쿼리를 JPQL 이라고 한다 > Java Persistence Language 
@Query(value="select * from Item where color = :color", nativeQuery=true)
List<Item> findAllItemByColor(String color);

'JPA' 카테고리의 다른 글

스프링 부트 + JPA(1)  (0) 2023.07.18

이전 글을 이어서 진행한다. 인스턴스 생성까지 진행하였다. 생성된 인스턴스는 인스턴스 페이지에서 연결 버튼을 누르면 AWS 사이트 상에서 원격 연결을 할 수 있다.

 

인스턴스 접속

연결
연결
연결된 모습

이렇게 aws 사이트 상에서 원격 접속이 가능하지만 putty나 MobaXterm이라는 원격접속 프로그램을 이용하는 것이 더 일반적이다. putty나 MobaXterm툴 둘다 유명한 툴인데 개인적으로는 파일 업로드의 간편함으로 인해 MobaXterm이라는 툴을 선호한다. 이 부분에 대해서는 일단은 지금은 넘어가고 나중에 다루려고 한다. 

 

탄력적 IP

인스턴스 페이지 우측 상단쪽에 보면 인스턴스 중지, 시작, 재부팅, 종료 등의 탭이 있다. 가끔 서버가 먹통이 될 경우에 인스턴스 중지 혹은 재부팅을 하는 경우가 있을 수 있다. 근데 처음 인스턴스를 생성하고 해당 인스턴스에 대하여 탄력적 IP를 부여하지 않으면 인스턴스 중지할 때마다 IP가 바뀌니까 조심하도록 해야한다. 만약에 EC2에서 특정 서비스를 할 것이라면 반드시 할당해야 하는 것이 탄력적 IP라고 봐도 무방하다. 그리고 저기 탭에서 인스턴스 종료는 인스턴스 삭제이니 절대로 누르지 말 것...

인스턴스

탄력적 IP 할당은 네트워크 및 보안 탭에서 탄력적 IP를 선택하고 탄력적 IP 페이지에서 탄력적 IP 주소를 할당하고 생성된 탄력적 IP를 가지고 탄력적 IP주소 연결을 하여 기존의 인스턴스에 할당하면 된다. (주소 할당 및 연결과정은 생략)

탄력적 IP

탄력적 IP가 인스턴스에 연결이 되면 이제 인스턴스를 중지 시키더라도 IP가 바뀌지 않는다. 이렇게 할당된 탄력적 IP는 1개까지는 무료로 이용할 수 있으나 2개를 사용하는 것부터 과금이 발생된다. 그리고 반드시 탄력적 IP가 할당된 상태에서는 인스턴스와 연결시켜야 하며 사용상태 이어야 한다. 만약에 탄력적 IP를 할당하였는데 인스턴스를 중지 시킨다면 과금이 발생할 것이다. 즉 사용하지 않는 탄력적 IP는 과금의 대상이라는 것이니 주의하자. 

JPA는 Java Persistent API의 약자이고 자바 진영에서 ORM(object-relational mapping) 기술 표준으로 사용되는 인터페이스이다. JPA를 구현하는 기술로 대표적인 오픈소스는 Hibernate가 있다. 레거시 프로젝트에서는 많이 사용하지 않으나 해외에서나 아니면 서비스업 회사나 스타트업 회사에서는 많이 사용한다고 한다. JPA, Mybatis의 통계적인 사용 비율이 3:7이라고는 말이 전해져 내려오나 실제로 그런지는 잘 모르겠다. 

  • ORM(Object Relational Mapping)
    • Entity 객체외 Database 테이블을 서로 매핑해서, SQL 쿼리가 아닌 Java 메소드를 사용해서 데이터를 조작한다.
    • DB의 데이터 구조를 자바의 객체 구조로 취급한다. 

프로젝트 셋팅 

STS4)
New > New Spring Starter Project
- Name : boot-jpa
- Type : Maven
- Java Version : 11 
- Packaging : Jar
- Group : com.test
- Aritifact : boot-jpa
- Package : com.test.jpa

Dependency 추가
- Spring Web
- Lombok
- Mabatis Framework
- Oracle Driver
- Spring boot Devtools
- Thymeleaf
- Spring JPA

DB 셋팅(Oracle 11g)

create table Item (
    name varchar2(30) primary key,
    price number not null,
    color varchar2(30) not null,
    owner varchar2(30) null,
    orderdate date default sysdate not null
);

insert into Item (name, price, color, owner, orderdate) values ('키보드', 100000, 'black', '홍길동', sysdate - 1);
insert into Item (name, price, color, owner, orderdate) values ('무접점 고급 키보드', 120000, 'white', null, sysdate - 1.5);
insert into Item (name, price, color, owner, orderdate) values ('기계식 키보드', 150000, 'black', '홍길동', sysdate - 2);
insert into Item (name, price, color, owner, orderdate) values ('멤브레인 키보드', 80000, 'white', '홍길동', sysdate - 2.5);
insert into Item (name, price, color, owner, orderdate) values ('마우스', 50000, 'silver', '홍길동', sysdate - 3);
insert into Item (name, price, color, owner, orderdate) values ('버티컬 마우스', 90000, 'silver', '아무개', sysdate - 3.5);
insert into Item (name, price, color, owner, orderdate) values ('게이밍 마우스', 120000, 'black', '아무개', sysdate - 4);
insert into Item (name, price, color, owner, orderdate) values ('고급 볼 마우스', 95000, 'yellow', '아무개', sysdate - 3);
insert into Item (name, price, color, owner, orderdate) values ('노트북', 1100000, 'white', '아무개', sysdate - 4.5);
insert into Item (name, price, color, owner, orderdate) values ('노트북 가방', 120000, 'blue', null, sysdate - 5);
insert into Item (name, price, color, owner, orderdate) values ('노트북 받침대', 95000, 'yellow', '하하하', sysdate - 5.5);
insert into Item (name, price, color, owner, orderdate) values ('노트북 파우치', 95000, 'yellow', '하하하', sysdate - 5);


select * from Item;

commit;

application.properties

# Server port
server.port=8092

# Oracle settings
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521/xe
spring.datasource.username=hr
spring.datasource.password=java1234

# 히카리cp가 있어서 이건 필요가 없다. >>> 이게 commons dbcp이다. 
# 아래것이 히카리CP이다.  

# Hikari CP settings
spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.maximum-pool-size=5
spring.datasource.hikari.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.hikari.jdbc-url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.hikari.username=hr
spring.datasource.hikari.password=java1234


# logging > 이게 log4j관련이다. 
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
logging.level.org.hibernate.SQL=debug

logging.level.org.hibernate.type.descriptor.sql=trace
logging.level.=error


#Thymeleaf
spring.thymeleaf.cache=false

# JPA
spring.jpa.database=oracle

spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
# jpa가 자동으로 만들어주는 쿼리를 보여주라 ~ !

# DB마다 자기들만의 구문이 있다. 이걸 넣어줘야 한다. 
spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect

프로젝트 파일 구조 

src/main/java
	com.test.controller
    	> ItemController
	com.test.domain
    	> Item.java
	com.test.jpa
    	> BootJpaApplication.java
	com.test.repository
    	> ItemRepository.java
        
src/main/resources
	templates
    	> item
        	> result.html
  • BootJpaApplication.java
    • controller 부분은 ComponentScan
    • Entity는 EntityScan
    • EnableJpaRepositories에 Repository 정의
@SpringBootApplication
@ComponentScan(basePackages = "com.test.controller")
@EntityScan(basePackages ="com.test.domain")
@EnableJpaRepositories(basePackages="com.test.repository")
public class BootJpaApplication {
	public static void main(String[] args) {
		SpringApplication.run(BootJpaApplication.class, args);
	}
}
  • com.test.repository > ItemRepository.java
  • JpaRepository<Item, String> 
    • 첫번째는 제네릭에 사용하려는 객체, 두번째는 PK 타입의 자료형을 쓴다. 
  • @Repository라는 어노테이션이 없어도 IoC된다. 이유는 JpaRepository를 상속했기 때문에 가능하다. 
public interface ItemRepository extends JpaRepository<Item, String>{
	
	
}
  • domain 패키지에는 엔터티를 정의한다. 
    • com.test.domain > Item.java
  • domain에 정의된 엔터티를 가져다가 사용하는 DAO역할을 하는 것이 필요하다. 이것을 repository 패키지에서 클래스를 정의한다.
    • com.test.repository > ItemRepository.java(I)
    • ItemRepository는 JpaRepository<Item, String>를 상속받아서 정의 한다. 
    • JpaRepository의 내부에 정의된 메서드들은 findAll, findAllById, saveAndFlush, deleteAllInBatch 등등 이상한 것들이 있다. findAll만 주로 많이 쓰는 듯하다. 
    • ItemRepository에 뭔가의 약속된 형식의 메서드명을 지어서 사용한다. Overriding은 아니지만 JPA에서 이 뭔가의 규칙에 맞게 처리를 해준다.
      • findBy○○○
      • findBy○○○Null
      • findAllByOrderBy○○○
      • findAllByOrderBy○○○Desc
      • findBy○○○In
  • 아래 예제 진행하다가 JpaRepository 자체가 상속 및 인터페이스 구조라는 것을 확인하고 검색한 이미지

아니 .. 이런구조가..

  • CrudRepository 
    • existsById, findById, save, saveAll, findAll, count, deleteById, delete, deleteAll 등의 메서드를 가지고 있다.
    • 이것을 상속받아서 내가 구현하는 Repository에서 별다른 메서드 정의없이 사용할 수 있다.
    • 단, 이런데에 해당하지 않는 Column에 따른 재정의가 필요한 메서드 정의시에는 정의를 해야 하는 것이다.
  • item/result.html
<!DOCTYPE html>
<html xmlns:th="https://thymeleaf.org">
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1>Spring Boot<small>JPA</small></h1>
	
	<div th:text="${bool}"></div>
	
	<div th:text="${count}"></div>
	
	<div class="message" title="결과">
		<!-- 다른애 도움없이 얘 혼자 쓸 수 있다. -->
		<th:block th:if="${result != null}">
		
			<div th:text="${result != null}"></div>
			<div th:text="${result.name}"></div>
			<div th:text="${#numbers.formatInteger(result.price, 0, 'COMMA')}"></div>
			<div th:text="${result.color}"></div>
			<div th:text="${result.owner}"></div>
			<div th:text="${result.orderdate}"></div>
			
		</th:block>
	</div>
	
	<hr>
	
	<table class="vertical" th:if="${list != null}">
		<tr>
			<th>이름</th>
			<th>가격</th>
			<th>색상</th>
			<th>소유자</th>
			<th>주문날짜</th>
		</tr>
		
		
		<tr th:each="item : ${list}">
			<td th:text="${item.name}"></td>
			<td th:text="${#numbers.formatInteger(item.price, 0, 'COMMA')}"></td>
			<td th:text="${item.color}"></td>
			<td th:text="${item.owner}"></td>
			<td th:text="${item.orderdate}"></td>
		</tr>
		
	</table>
	
</body>
</html>
  • com.test.domain > Item.java
    • lombok Builder 패턴 사용
    • @Entity  > 테이블 정의 >>> 객체화 
    • @NoArgsConstructor, @AllArgsConstructor 생성자 정의
    • @Id 
      • Primary Key 역할을 하는 칼럼에 붙인다.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Item {
	// 엔티티가 참조하는 테이블의 컬럼 > 멤버 정의
	// PK는 꼭 붙여야 한다. 
	@Id
	private String name;
	
	private int price;
	private String color;
	private String owner;
	
	@Column(insertable=false, updatable=false) // 얘를 업데이트하는 대상에서 뺴버린다.
	private String orderdate; // 이 컬럼은 insert를 안해도 된다. default가 있다. 
}
  • ItemController.java 클래스 내부 
    • @EnableJpaRepositories를 BootJpaApplication에 정의됨에 따라 스캔이 가능해짐
    • 이걸 사용하는 쪽(Controller나 Service로직)에서 @Autowired 하여 쓴다.
  • JPA는 쿼리들을 메서드화 시켰다.
    • insert(DB) > (JPA) > save(Method)
    • <S extends T> S save(S entity)
  • m1 > 객체 setter로 정의 > 레코드 Insert : [C]RUD
@Autowired
private ItemRepository itemRepo;

@GetMapping("/item/m1")
public String m1(Model model) {
    System.out.println("m1");
    //[C]RUD
    // - 레코드 추가하기 
    Item item = new Item();
    item.setName("잉크젯 프린터");
    item.setPrice(250000);
    item.setColor("yellow");
    item.setOwner("홍길동");
    // save > (JPA) > insert
    Item result = itemRepo.save(item);
    // Hibernate: insert into item (color, orderdate, owner, price, name) values (?, ?, ?, ?, ?)
    // orderdate date default sysdate not null > db엔 이렇게 되어 있다. 
    model.addAttribute("result", result);

    return "item/result";
}

결과

  • m2 > 객체 Builder 패턴으로 정의 > 레코드 Insert  : [C]RUD
    • Builder 패턴 사용으로 인한 객체 값 셋팅은 기존 생성자 방식보다 가독성이 좋다. 
	@GetMapping("/item/m2")
	public String m2(Model model) {
		System.out.println("m2");
		//[C]RUD
		// - 레코드 추가하기 
		//빌더 패턴(Builder Pattern)
		// 생성자 패턴, 빌더 패턴 
		// - OOP에서 겍체를 생성하는 패턴 중 하나
		Item item2 = new Item(null, 0, null, null, null);		
		// 얘는 멤버를 자유롷게 넣을 수 있어서 필요없으면 뺴고 안넣어도 된다. 가독성있다.
		Item item = Item.builder()
				.name("레이저 프린터")
				.price(300000)
				.color("black")
				.owner("아무개")
				.build();
		// save > (JPA) > insert
		Item result = itemRepo.save(item);
		// Hibernate: insert into item (color, orderdate, owner, price, name) values (?, ?, ?, ?, ?)
		// orderdate date default sysdate not null > db엔 이렇게 되어 있다. 
		model.addAttribute("result", result);
		return "item/result";
	}
  • m3 > 단일 레코드 읽기 > findById("뭘로?") : C[R]UD
    • Optional<T> findById(ID id); 리턴형이 Optional이라는 것 주의! 
      • .get()으로 값을 꺼낸다는 것
    • itemRepo.getById("마우스") : 이것도 되지만 Deprecated 된 메서드이다. > 비권장
      • 이건 T getById(ID id);
	@GetMapping("/item/m3")
	public String m3(Model model) {
		//C[R]UD
		// - 단일 레코드 읽기
		//	select item0_.name as name1_0_0_, item0_.color as color2_0_0_, item0_.orderdate as orderdate3_0_0_, item0_.owner as owner4_0_0_, item0_.price as price5_0_0_ from item item0_ where item0_.name=?
		//	Hibernate: select item0_.name as name1_0_0_, item0_.color as color2_0_0_, item0_.orderdate as orderdate3_0
		// 내부적으로 쿼리로 동작한다. 메소드가 더이상 안쓰인다. 
		//Item item = itemRepo.getById("마우스"); // 얘는 더이상 안쓰임
		//model.addAttribute("result", item);
		Optional<Item> item = itemRepo.findById("프린터"); // ★ 얘는 가장 많이 쓰인다. 레코드 찾을 때
		System.out.println(item.isPresent());
		model.addAttribute("result", item.get());
		return "item/result";
	}

결과

  • m4 > isPresent() : C[R]UD 검색한 결과가 존재하는지 확인하는데 사용할 수 있다. 
  • Optional의 isPresent()
@GetMapping("/item/m4")
	public String m4(Model model) {

		Optional<Item> item = itemRepo.findById("프린터");
		
		if(item.isPresent()) {
			item.get().setPrice(240000);
			Item result = itemRepo.save(item.get());
			model.addAttribute("result", result);
		}
		
		return "item/result";
}

 

결과

  • m5 > delete(엔터티) > :  CRU[D] 해당 레코드를 찾고 그것을 인자로 넣어서 삭제를 한다. 
  • Optional로 찾으니까 get()으로 꺼내서 사용
@GetMapping("/item/m5")
public String m5(Model model) {
    //CRU[D]
    Optional<Item> item = itemRepo.findById("노트북");
    itemRepo.delete(item.get());

    return "item/result";
}
  • m6 > 다중 레코드 조회 > findAll() > C[R]UD 
  • findAll()은 JpaRepository에서 정의된 메서드로 이건 ItemRepository에 따로 정의할 필요가 없다.
@GetMapping("/item/m6")
	public String m6(Model model) {
		//Query Methods
		//Repository에 구현된 메서드가 쿼리 메서드다.
		// 다중 레코드 조회
		List<Item> list = itemRepo.findAll();
		model.addAttribute("list", list);
		return "item/result";
}
  • m7 > 존재유무 확인 > existById(뭐로?) : C[R]UD 
@GetMapping("/item/m7")
public String m7(Model model) {
    // 존재 유무 확인
    boolean bool = itemRepo.existsById("마우스");
    model.addAttribute("bool", bool);
    return "item/result";
}

결과

  • m8 > count() > 이거나 existById() 이런것들은 CrudRepository 에 정의된 것을 사용한 것이다.
@GetMapping("/item/m8")
	public String m8(Model model) {
		// 카운트
		long count = itemRepo.count();
		model.addAttribute("count", count);
		return "item/result";
}

결과

이제부터는 사용자 정의 findBy○○○ 메서드 

여기서부터는 다음 글에 이어서 진행

 

소스코드는 https://github.com/ngotic/SpringBootBasic/tree/main/boot-jpa

'JPA' 카테고리의 다른 글

스프링 부트 + JPA(2)  (0) 2023.07.19
  • Controller
@GetMapping("/m10")
public String m10(Model model) {
    List<String> names = mapper.getNames();
    List<BoardDTO> list = mapper.getList();
    model.addAttribute("names", names);
    model.addAttribute("list", list);
    return "m10";
}
	
@GetMapping("/m11")
public String m11(HttpSession session) {
    session.setAttribute("id", "hong");
    // session.invalidate();
    return "m11";
}

@GetMapping("/m12")
public String m12() {
    return "m12";
}
  • MyBatis
<select id="getNames" resultType="String">
	select first_name from employees where rownum &lt;= 10
</select>


<select id="getList" resultType="dto">
    select * from tblBoard
</select>
  • th:each="엘리먼트 : ${리스트배열}" 이게 기본 형태이다.
    • 태그 하나로 바로 사용가능하고 자식 태그를 두어서 사용도 가능하다.
<ul>
    <li th:each="name : ${names}" th:text="${name}">이름</li>
</ul>

<ul>
    <li th:each="name : ${names}" >
        <span th:text="${name}"></span>
    </li>
</ul>
---------------------------------------------------------------
<ul th:each="name: ${names}">
    <li th:text=${name}></li>
</ul>

결과

  • 아래의 모형이 dto 리스트를 받아서 출력하는데 th:object 사용하면 선택 변수 표현식으로 * 지정해서 사용가능 
<ul>
    <li th:each="dto : ${list}" th:text="|${dto.subject}(${dto.id})|"></li> 
</ul>
<ul>
    <li th:each="dto : ${list}" th:object="${dto}">
        <span th:text="*{subject}"></span>
    </li>
</ul>
<ul>
    <li th:each="dto : ${list}" th:object="${dto}" th:text="*{subject}">
    </li>
</ul>

결과

  • jstl 과 마찬가지로 status라는 것이 있다. index, count 꺼내쓰고 해당 넘버가 어떤 속성인지 last, odd, first even 등을 정의해놓음
<table>
    <tr>
        <th>번호</th>
        <th>아이디</th>
        <th>제목</th>
        <th></th>
        <th></th>
        <th></th>
        <th></th>
        <th></th>
        <th></th>
        <th></th>
    </tr>
    <!-- 콤마로 두개 가능  -->
    <tr th:each="dto, status : ${list}">
        <td th:text="${dto.seq}"></td>
        <td th:text="${dto.id}"></td>
        <td th:text="${dto.subject}"></td>

        <td th:text="${status.index}"></td>
        <td th:text="${status.count}"></td>
        <td th:text="${status.size}"></td>

        <td th:text="${status.even}"></td>
        <td th:text="${status.odd}"></td>
        <td th:text="${status.first}"></td>
        <td th:text="${status.last}"></td>
    </tr>
</table>
  • ${#numbers.sequence(1,5)} 라는 걸 정의해서 1~5까지 출력이 가능하다. 
<th:block th:each="num : ${#numbers.sequence(1,5)}">
    <div th:text="${num}"></div>
</th:block>

결과

  • 마찬가지로 내장객체라는 것을 가리킬 수 있어야 하는데 ${  }안에 #을 붙인다. 
  • 단 session은 #을 붙이지 않는다. 
<div th:text="${#request}"></div>
<div th:text="${#response}"></div>
<div th:text="${#locale}"></div>
<div th:text="${session}"></div>
<div th:text="${#servletContext}"></div>

<div th:if="${session.id != null}">
    인증 : <span th:text="${session.id}">아이디</span>
</div>

<div th:unless="${session.id != null}">미인증</div>

결과

  • ~{    } : 조각 페이지를 삽입할 때 쓰는 include 지시자이다. 
    • insert나 replace는 거의 동일하다. ( 차이가 뭐지..? )
    • ~{파일이름} 이렇게 쓰면 확장자는 생략할 수 있다. 
    • th:insert, th:replace로 ~{ } 생략한 채로 th:insert그냥 바로 참조가 가능하다.
  • th:fragment="프래그먼트이름"
    • th:fragment 라는게 있고 이것은 조각이다. 이걸 따로 정의하고 가져다가 쓸 때는 첫번째로는 문서참조, 그 후에 : : 을 붙이고 해당 fragment 이름으로 참조한다. :: 으로 namespace를 찾는 느낌이다.
  • ★★ fragment에 파라미터를 넘겨줄 수 있다.
    • th:fragment="owner(name, tel)" 이런식으로 정의해주면 인자를 name, tel로 넘겨받겠다라는 뜻
    • 예를 들어서 username 같은거만 따로 넘겨줘서 로그인한 유저에 따라 표기를 다르게 할 수 도 있을 듯..
<!-- templates/inc/sub.html -->
<div>조각 페이지</div>
<!-- sub2.html -->
<div th:fragment="part">조각 페이지2</div>
<div th:fragment="part2">조각 페이지3</div>
<div th:fragment="owner(name, tel)">
    <div>소유주 : <span th:text="${name}"></span></div>
    <div>연락처 : <span th:text="${tel}"></span></div>
</div>
<!-- m12.html -->
<h1>Thymeleaf Fragment</h1>
<h2>insert</h2>
<div th:insert="~{inc/sub.html}"></div>
<!-- insert replace 차이 -->
<h2>replace</h2>
<div th:replace="~{inc/sub.html}"></div>
<hr>

<!-- 확장자 생략이 돤다, -->
<div th:insert="~{inc/sub}"></div>

<!-- ~{} 생략 가능 > 비권장 -->
<div th:insert="inc/sub"></div>
<div th:insert="inc/sub2.html :: part"></div>
<div th:insert="inc/sub2.html :: part2"></div>
<hr>
<!-- 사용시에 약간 함수느낌으로 인자를 넣어줌 -->
<div th:insert="~{inc/sub2::owner('아무개', '010-3333-4444')}"></div>
<div th:insert="~{inc/sub2::owner('테스트', '010-3333-4444')}"></div>

결과

 

  • Controller
@GetMapping("/m7")
	public String m7(Model model) {
		int num1= 1234567;
		double num2 = 12345.6789;
		Calendar now = Calendar.getInstance();
		model.addAttribute("num1",num1);
		model.addAttribute("num2",num2);
		model.addAttribute("now",now);
		return "m7";
}

@GetMapping("/m8")
	public String m8(Model model) {
		int seq = 10;
		String mode = "add";
		model.addAttribute("seq",seq);
		model.addAttribute("mode",mode);
		return "m8";
}
	
@GetMapping("/m9")
public String m9(Model model) {
    int num1 = 10;
    int num2 = 5;
    String mode = "add";
    model.addAttribute("num1", num1);
    model.addAttribute("num2", num2);
    model.addAttribute("mode", mode);
    return "m9";
}
  • 숫자 형식 변환하기 m7.html
    • #numbers.formatDecimal(표시 값, 최소 정수 자릿수, 최소 소수 자릿수)
    • 추가로 가능한 것
      • formatCurrency, formatPercent 이런것도 가능하다. 
      • ${#numbers.arrayFormatDecimal(numArray,3,2)}
      • ${#numbers.listFormatDecimal(numList,3,2)}
      • ${#numbers.setFormatDecimal(numSet,3,2)}
<h2>숫자</h2>
<div th:text="${num1}"></div>
<div th:text="${#numbers.formatInteger(1, 3, 'COMMA')}"></div>    
<!-- 남은 자리 0으로 채운다. 최대 3자리 --> 
<div th:text="${#numbers.formatCurrency(1)}"></div>
<div th:text="${#numbers.formatPercent(num1, 5, 1)}"></div>
<div th:text="${#numbers.formatInteger(num1, 3, 'COMMA')}"></div>
<!-- 두번쨰 인자는 표현자리수다. -->
<div th:text="${num2}"></div>
<div th:text="${#numbers.formatDecimal(num2, 3, 'COMMA', 1, 'POINT')}"></div>
<!-- 3자리씩콤마로 짜르고, 1자리 소수부분 나타낸다. -->

 

결과

  • 날짜관련
    • #dates.메서드로 날짜 객체를 받아서 사용한다.
<h2>날짜</h2>
<div th:text="${now}"></div> 
<div th:text="${#dates.year(now)}"></div>
<div th:text="${#dates.month(now)}"></div>
<div th:text="${#dates.monthName(now)}"></div>
<div th:text="${#dates.monthNameShort(now)}"></div>
<div th:text="${#dates.day(now)}"></div>
<div th:text="${#dates.hour(now)}"></div>
<div th:text="${#dates.minute(now)}"></div>
<div th:text="${#dates.second(now)}"></div>
<div th:text="${#dates.millisecond(now)}"></div>
<div th:text="${#dates.dayOfWeek(now)}"></div>
<div th:text="${#dates.dayOfWeekName(now)}"></div>
<div th:text="${#dates.dayOfWeekNameShort(now)}"></div>
<div th:text="${#dates.format(now)}"></div>
<div th:text="${#dates.format(now, 'yyyy-MM-dd HH:mm:ss')}"></div>
<div th:text="${#dates.format(now, 'yyyy-MM-dd aa hh:mm:ss')}"></div>

결과

  • Link URL Expression
    • @{   }  
    • a태그에서 URL 표현
      • 매개변수 처리가 쉽고 Context Root Path가 자동으로 삽입된다. 
      • 쿼리 스트링을 붙이는 표현 가능
      • ★★★ th:href와 같이 쓴다. 
      • @{url(key1=value1, key2=value2)}
  • m8.html
 <div><a href="/m7">이전 페이지</a></div>
 <div><a href="/spring/m7">이전 페이지</a></div>

 <!-- root context를 붙여준다. -->
 <div><a th:href="@{/m7}">이전 페이지</a></div>
 <hr>
 <h3>QueryString, 매개변수</h3>
 <div>
    <a href="/m7?seq=100">이전 페이지</a>
    <a href="/m7?seq=${seq}">이전 페이지</a> <!-- 잘못된 표현 -->
    <a th:href="@{/m7(seq=${seq})}">이전 페이지</a>

    <a href="/m7?seq=100&mode=add">이전 페이지</a>
    <a th:href="@{/m7(seq=${seq}, mode=${mode})}">이전 페이지</a>
 </div>
 <!-- 
    기본 앱(QueryString)
    - /m7?seq=10

    REST(Path Variable)
    - /m7/10
  -->
 <div>
    <!-- 바인딩이 되버린다. -->
    <a th:href="@{/m7/{seq}(seq=${seq})}">이전 페이지</a>
    <a th:href="@{/m7/{mode}/{seq}(seq=${seq},mode=${mode})}">이전 페이지</a>
 </div>

소스검사

  • th:if, th:unless
    • 조건을 만족하면 해당 태그가 만들어지고 만족하지 못하면 해당 태그가 사라진다. 
    • if와 else 연달아서는 아래처럼 쓴다.
      • <div th:if="${num1 > 0}">양수</div>
      •  <div th:unless="${num1 > 0}">음수</div>
  • th:switch
    • th:switch로 받아서 th:case로 매칭
  • m9.html
 <h2>if</h2>
 <div th:if="${num1 > 0}">num1 : 양수</div>
 <div th:if="${num2 < 0}">num2 : 양수</div>
 <div th:if="${num1 > 0}">
    <span th:text="'양수' + ${num1} + '입니다.'"></span>
 </div>
 <!-- 조건을 만족못하면 테그 자체가 사라진다. 조건을 만족하면 태그가 만들어진다. -->
 <div th:if="${num1 > 0}" th:text="'양수' + ${num1} + '입니다.'"></div>
 <div th:if="${num1 > 0}" th:text="|양수 + ${num1} + 입니다.|"></div>
 <div th:if="${num1 > 0}">양수 [[${num1}]]입니다.</div>
 <hr>	 
 <div th:if="${num1 > 0}">양수</div>
 <div th:unless="${num1 > 0}">음수</div>
 <!-- 이걸 만족하지 못했을 때 실행된다. -->
 <hr>
 <h2>switch</h2>
 <div th:switch="${mode}">
    <div th:case="add">추가하기</div>
    <div th:case="remove">삭제하기</div>
    <div th:case="*">기타</div>
 </div>

소스검사

  • Controller
@GetMapping("/m5")
	public String m5(Model model) {
		model.addAttribute("name", "age");
		model.addAttribute("size", 30);
		model.addAttribute("color", "cornflowerblue");
		return "m5";
}
  • HTML 속성조작
    • th:text="값"
    • th:HTML속성명="값"
    • 기존에 동일한 속성이 선언되어 있으면 대체한다.
    • 기존에 동일한 속성이 선언되어 있지 않으면 추가한다. 
    • ★ 대부분의 경우에는 서버에서 Model로 전송하여 받은 값은 그냥 사용할 수 없다. 
      • 직접 표현식으로 ${key} 형태로는 바로 못쓴다.(일반속성에 표현식 적용이 안된다.)
      • th:속성으로 표현식을 넣어야 가능하다. 
<div>${name}</div>
<div th:text="${name}"></div>

<input type="text" name="age">     <!-- 이건 정적 -->
<input type="text" th:name="age">  <!-- 이건 타임리프가 손을 대서 만듬 -->
<input type="text" name="${name}"> <!-- 이건 타임리프 속성이라 안된다. -->
<input type="text" th:name="${name}"> <!-- 이건 타임리프 속성이다. -->
<input type="text" th:name="${name}" th:size="${size}">  
<input type="text" th:value="${color}">
  • 위의 코드는 아래와 같이 해석된다. th:name="age" 이렇게 타임리프 속성에 정적인 값을 넣어도 적용이 되지만 일반 속성에 표현식을 넣으면 적용이 되지 않는다.

해석

 

  • th:class 
    • 특이한 점이 있다면 한번더 정의하게 되면 나중에 나온것이 적용이 된다. 
<div class="one">Box 1</div>
<div th:class="one">Box 2</div>
<div class="one" th:class="two">Box 3</div>

적용

  • 기존에 jquery 하던 class append 같은 것들을 하는 append 속성
    • th:attrappend > 뒤
    • th:attrprepend > 앞 
    • th:classappend > 뒤
<div class="one" th:attrappend="class=' two'">Box 4</div>
<div class="one" th:attrprepend="class='two '">Box 5</div>
<div class="one" th:classappend="two">Box 6</div>

결과

  • 속성 설정 및 css 스타일 설정
    • th:checked는 타임리프에 의해서 속성이 뭔가 새롭게 가공되는 느낌이다. 
      • true면 checked="checked", false면 아예 나타나지 않는다. 
    • th:style을 넣을 때는 그냥 맘 편하게 |  |을 사용하고 그렇게 쓰지 않으면 문자열 + 로 처리
<input type="checkbox" name="cb" th:checked="true">
<input type="checkbox" name="cb" th:checked="false">

<div th:style="'background-color:' +${color}">Box 7 </div> 
<div th:style="|background-color:${color}|">Box 7 </div>

결과

  • Controller
@GetMapping("/m6")
	public String m6(Model model) {
		String name = mapper.getTxt();
		BoardDTO dto = mapper.getDTO();
		String txt = "안녕하세요. &lt;i&gt;홍길동</i> 입니다.";
		int num = 100;
		List<String> names = mapper.getNames();
		model.addAttribute("name", name);
		model.addAttribute("dto", dto);
		model.addAttribute("txt", txt);
		model.addAttribute("num", num);
		model.addAttribute("names", names);
		return "m6";
}
  • getNames 부분의 쿼리정의 ↓
<select id="getNames" resultType="String">
    select first_name from employees where rownum &lt;= 10
</select>
  • th:text는 기본적으로 escape 되어 텍스트가 출력된다.
    • 해석이 안되고 escape 된다. > CDATA 느낌
  • th:utext 
    • unescaped text으고 PCDATA로 해석이 되어 출력이 된다. >> script injection이 된다.
<div th:text="${txt}"></div>
<!-- 이스케이프 되서 출력 -->
<div th:utext="${txt}"></div>
<!-- 이스케이프 안되서 출력 i태그도 나온다.-->

결과

  • th:inline
    • 1. th:inline="text" // html 쪽에서 쓴다. 사용 잘 안한다.
    • 2. th:inline="javascript" // script 쪽에서 쓴다. 많이 쓴다.(필수) ★★★★
  • escape text, unescape text를 태그 사이 내용으로 넣을 때 아래의 키워드를 쓴다. (th:inline이 함께 쓰이면서) 안쪽에 표현식을 넣는다. 
    • [[ ]] : escaped text
    • [( )] : unescaped text
    • ★★★ 이런 것들 주석처리는 타임리프 주석처리로 해야한다. <!-- --> 와 같은 일반주석 처리시에는 에러난다. 
    • <!--/*        주석할 내용            */--> 
    • <!--/* <div th:inline="text">[[${a}]] + [[${b}]] = [[${a + b}]]</div> */-->
<div th:inline="text">[[${name}]]</div>
<div th:inline="text">[(${name})]</div>

<div th:inline="text">[[${txt}]]</div>
<div th:inline="text">[(${txt})]</div>

<div>[(${txt})]</div>
<div th:inline="text">[[${num}]] + [[${num}]] = [[${num + num}]]</div>

결과

  • th:inline="javascript"를 쓴 것과 안쓴 것의 차이 ★
    • label1, label2에 값을 넣는 예제인데 기존에 표현식을 script 단에서 썼을 때 문자열 표현식을 '  '로 감싸야 했던 상황이 있었는데 th:inline="javascript"을 쓰면 문자열은 자동으로 " "를 붙여서 script단에서 동작한다. 숫자는 그냥 나온다. ( th:inline="javascript"에서 [[]] 사용시 한글은 퍼센트 인코딩이 되어서 나오니까, 주의할 것! )
    • th:inline="javascript"을 사용하고 [[ ]], [( )]을 가지고 적절하게 맞게 사용해야 한다.
      • [( )] 형태로 꺼내쓰면 인코딩 없이 데이터가 나온다. [[ ]] 로 꺼내쓰면 인코딩 되어 나온다.
      • [( )] 형태로 쓰려거든 ' '를 직접 붙여줘야 한다. 
<div id="label1"></div>
<div id="label2"></div>
	
	
<script>
	let name1 = '[[${name}]]';
	let num1 = [[${num}]];
	let names1 = '[[${names}]]';
	let dto1 = '[[${dto}]]';
	// 소스 보기에서 자바스크립트 배열이 아니다. 
	document.getElementById('label1').textContent = name1;	
</script>

<script th:inline="javascript">
    //let name2 = '[[${name}]]'; // 이거 쓰면 " " 붙어서 나온다.
    let name2 = [[${name}]]; // 이거 쓰면 " " 붙어서 나온다.
    let num2 = [[${num}]];
    let names2 = [[${names}]];
    let dto2 = [[${dto}]];
    let dto3 = '[(${dto})]'; // ' '를 안쓰면 문자열화가 안되어서 에러난다. 
    // 소스 보기에서 자바스크립트 배열이다. 손댈것이 없다.  
    document.getElementById('label2').textContent = name2;
</script>

 

결과

 

+ Recent posts