Spring Boot MVC(2)

강성관·2025년 3월 14일

Framework

목록 보기
5/11

Logging(Logback)

문제

  • 데이터 확인을 위해 표준 출력을 사용하는 경우
  • 출력 범위 조절 불가, 서버 성능 저하
  • 로그를 사용(Logging)

표준출력 장단점

  • 장점 : 빠르고 간단하게 확인 가능
  • 단점 : 로그 레벨 구분이 없고, 개발 환경에서는 비효율적, 서버 성능 저하

Logging 장단점

  • 장점 : 로그 레벨 설정, 파일 저장, 콘솔 출력, 다양한 포맷팅 등 관리가 뛰어남, 최적화 가능
  • 단점 : 설정이 다소 복잡할 수 있음

실무에서는 Logging이 기본이다.

로그 레벨

  • 로그 레벨에 따라 기록되는 로그가 달라짐
  • 일반 적으로 INFO레벨 사용.(로그 레벨=INFO 사용시 INFO를 포함한 상위 레벨들만 로깅 출력)
  • 전체 로그 레벨을 INFO로 설정하면 -> INFO WARN ERROR
  • 전체 로그 레벨을 ERROR로 올리면 -> ERROR

권장 사용

(낮은 레벨)
✅ DEBUG → 개발 중
✅ INFO → 시스템 상태 모니터링
✅ WARN → 잠재적 위험 요소
✅ ERROR → 오류 발생 (빠른 조치 필요)
✅ FATAL (SEVERE) → 심각한 장애 (즉각 대응 필요)
(높은 레벨)
✅ OFF - 로그 출력 없음


Spring Boot에서 로그 사용

BoardController 클래스 상단에 logger 객체 생성

private Logger logger 
	= LoggerFactory.getLogger(BoardController.class);

saved 객체를 logger로 기록 남기기

logger.debug("1 : "+saved.toString());
logger.info("2 : "+saved.toString());
logger.warn("3 : "+saved.toString());
logger.error("4 : "+saved.toString());

JPA 로깅 설정하기

  • JPA에 로깅을 설정하여 동작하는 쿼리 확인 가능
  • 기본들 로그 설정을 INFO로 logback-spring.xml에서 했고
  • SQL만 로그 설정을 DEBUG로 application.properties로 했다.

현재 사용 버전

  • slf4j
  • import org.slf4j.Logger;
  • import org.slf4j.LoggerFactory;

application.properties에 코드 추가

logging.level.org.hibernate.SQL=debug
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.orm.jdbc.bind=trace

JPA 날짜 정보 관리(1)

  • 기존 MariaDB 사용해서 날짜 정보를 CURRENT_TIMESTAMP 사용했음.
  • JPA에서는 두 가지 어노테이션을 사용한다. @CreationTimestamp, @UpdateTimestamp

@CreationTimestamp

  • save() 시점에서 자동으로 생성 시간 기록
  • 업데이트 시점에서도 동작하여 null이 될 가능성이 있다.
  • updatable = false 옵션을 필수로 한다.
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime regDate;

@UpdateTimestamp

  • save() 시점에서 자동으로 수정 시간 기록
  • 등록 시점에서도 동작하여 regDate랑 같은 값이 들어갈 수 있다.
  • insertable = false 옵션을 필수로 한다.
@UpdateTimestamp
@Column(insertable = false)
private LocalDateTime modDate;

JPA 연관관계

  1. 일대일(회원-주소)
  2. 일대다(사용자-게시글. 서로의 입장에서 보면 일대다=다대일 똑같음)
  3. 다대일(사용자-게시글)
  4. 다대다(학생-수업)

다대일(사용자-게시글) 상황의 예시 코드

Board(엔티티)

	@ManyToOne
	@JoinColumn(name="board_writer")
	private Member member;

Member(엔티티)

	@OneToMany(mappedBy = "member")
	private List<Board> boards;

board/list.html에 작성자 정보 추가

<td th:text="${board.member.memberName}">작성자</td>

검색 기능 추가

  • 조건문을 추가해야한다.
  1. 게시판 목록에 검색 관련 코드 추가
<!-- 검색 -->
<form action="/board" name="search_board_form" method="get" >
	<div class="search">
    	<select name="search_type">
				<option value="1" th:selected="${searchDto.search_type == 1}">제목</option>
				<option value="2" th:selected="${searchDto.search_type == 2}">내용</option>
				<option value="3" th:selected="${searchDto.search_type == 3}">제목+내용</option>
        </select>
        <input type="text" name="search_text" placeholder="검색어를 입력하세요." th:value="${searchDto.search_text}">
		<input type="submit" value="검색">
	</div>
</form>
  1. 검색 정보 전달을 위한 Dto 생성
public class SearchDto {
	private int search_type;
	private String search_text;
}
  1. 게시글 목록으로 검색 데이터 리스트를 보내주기
@GetMapping("/board")
public String selectBoardAll(Model model, SearchDto searchDto) {
	List<Board> resultList = boardService.selectBoardAll(searchDto);
	model.addAttribute("boardList", resultList);
	return "board/list";
}

JPA의 데이터 조회시 3가지 방법(where절 세팅)

  1. JpaRepository 메소드 네이밍
    • JpaRepository에 규칙 기반 메소드 추가
    • JPA는 메소드 이름을 분석하여 자동으로 쿼리 생성함
    • findBy + 필드명 + 키워드 형태로 작성
  2. JPQL을 사용한 검색
    • @Query를 사용하여 SQL문 직접 작성
    • JPQL을 사용한 검색
  3. Specification 사용
    • Specification을 사용하여 동적 검색 구현
    • Specification 사용
    • 동적 조건을 쉽게 조합하기 위해서 Specification API 사용

1. JpaRepository 메소드 네이밍

  • JpaRepository에 규칙 기반 메소드 추가
  • JPA는 메소드 이름을 분석하여 자동으로 쿼리 생성함
  • findBy + 필드명 + 키워드 형태로 작성

키워드

  • Containing(LIKE %+'여기'+%) ex)findByBoardTitleContaining
    - 특정 문자열이 포함
  • StartingWith
    - 특정 문자열로 시작
  • EndingWith
    - 특정 문자열로 끝
  • GreaterThan
    - 특정 값보다 큰 경우
  • LessThan
    - 특정 값보다 작은 경우
  • OrderBy
    - 정렬
  • And
    - 여러 조건 And로 연결
  • Or
    - 여러 조건 Or로 연결

2. JPQL을 사용한 검색

  • @Query를 사용하여 SQL문 직접 작성
  • JPQL을 사용한 검색

표현법

@Query(value = "SELECT 별칭 FROM 엔티티명 별칭 WHERE 별칭.필드명 = ?1")
리턴 메소드명(매개변수);
  • 쿼리에 매개변수로 전달해준 값을 쓰는 경우
  • ?1 과 같이 매개변수의 위치 기반으로 작성
  • :keyword 와 같이 매개변수의 이름을 기준으로 작성

주의사항 : ?1 과 :keyword 두 가지 방법을 혼용해서 사용하면 오류 가능성이 높다.

3. Specification 사용

  • Specification을 사용하여 동적 검색 구현
  • Specification 사용
  • 동적 조건을 쉽게 조합하기 위해서 Specification API 사용

동작 흐름
1. 검색 조건을 규정하는 메소드가 있는 클래스 생성
2. Repository에 JpaSpecificationExecutor 인터페이스 Implements
3. 검색 조건 메소드의 클래스 사용


1. BoardRepository에 커스텀 메소드 추가

package com.gn.mvc.repository;

import java.util.List;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;

import com.gn.mvc.entity.Board;

public interface BoardRepository extends JpaRepository<Board, Long>, JpaSpecificationExecutor<Board> {
	
//	3. Specification 사용 / 오버로딩 했다~ 이걸 서비스의 selectBoardAll 에서 썼다.
	List<Board> findAll(Specification<Board> spec);
	
	// 1. 메소드 네이밍. findBy + 필드명 + 키워드 형태
//	List<Board> findByBoardTitleContaining(String keyword);
//	List<Board> findByBoardContentContaining(String keyword);
//	List<Board> findByBoardTitleContainingOrBoardContentContaining(String titleKeyword, String contentkeyword);
	
	// 2. JPQL 이용
	// 엔티티를 기준으로 하는 거라서 * 대신 별칭 정한 b라고 한다.
	@Query(value="SELECT b FROM Board b WHERE b.boardTitle LIKE CONCAT('%',?1,'%')") // :을 쓰는 경우 매개변수 이름 맞춰줘야한다. 
//	@Query(value="SELECT b FROM Board b WHERE b.boardTitle LIKE CONCAT('%',?1,'%')")
	List<Board> findByTitleLike(String keyword);
	
	@Query(value="SELECT b FROM Board b WHERE b.boardContent LIKE CONCAT('%',?1,'%')")
	List<Board> findByContentLike(String keyword01);
	
	@Query(value="SELECT b FROM Board b WHERE b.boardTitle LIKE CONCAT('%',?1,'%') OR b.boardContent LIKE CONCAT('%',?2,'%')")
	List<Board> findByTitleOrContentLike(String title, String content);	
}

2. BoardService에 상황별 메소드 셋팅

package com.gn.mvc.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import com.gn.mvc.dto.BoardDto;
import com.gn.mvc.dto.SearchDto;
import com.gn.mvc.entity.Board;
import com.gn.mvc.repository.BoardRepository;
import com.gn.mvc.specification.BoardSpecification;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class BoardService {
//	롬복이 대신 해줌! @RequiredArgsConstructor
//	@Autowired
//	BoardRepository repository;
//	롬복이 대신 해줌! @RequiredArgsConstructor
	private final BoardRepository repository;
	
	// 어노테이션 뿐만 아니라 JpaRepository를 상속받은 Interface들은 Bean 스캐닝 없이 사용 가능
	
	
	public BoardDto createBoard(BoardDto dto) {
		// 1. 매개변수 dto -> entity
		Board param = dto.toEntity();
		// 2. Repository의 save() 메소드 호출(insert 쿼리 실행)
		Board result = repository.save(param);
		// 3. 결과 entity -> dto로 바꿔서 return
		return new BoardDto().toDto(result);
		
	}
	public List<Board> selectBoardAll(SearchDto searchDto){
//		1,2 방법
//		List<Board> list = new ArrayList<Board>();
//		if(searchDto.getSearch_type() == 1) {
//			// 제목 기준 검색
//			list = repository.findByTitleLike(searchDto.getSearch_text());
//		} else if(searchDto.getSearch_type() == 2) {
//			// 내용 기준 검색
//			list = repository.findByContentLike(searchDto.getSearch_text());
//			
//		} else if(searchDto.getSearch_type() == 3) {
//			// 제목+내용 기준 검색
//			list = repository.findByTitleOrContentLike(searchDto.getSearch_text(),searchDto.getSearch_text());
//			
//		} else {
//			// WHERE절 없이 검색(처음 진힙 했을 때)
//			list = repository.findAll();
//		}
//		return list;
		
		
//		3 방법
		Specification<Board> spec = (root, query, criteriaBuilder) -> null;
		if(searchDto.getSearch_type() == 1) {
			spec = spec.and(BoardSpecification.boardTitleContains(searchDto.getSearch_text()));
		} else if(searchDto.getSearch_type() == 2) {
			spec = spec.and(BoardSpecification.boardContentContains(searchDto.getSearch_text()));
		} else if(searchDto.getSearch_type() == 3) {
			spec = spec.and(BoardSpecification.boardTitleContains(searchDto.getSearch_text()))
					.or(BoardSpecification.boardContentContains(searchDto.getSearch_text()));
		}
		List<Board> list = repository.findAll(spec);
		return list;
		
	}
}

3. list.html 검색창에 데이터 반영

package com.gn.mvc.repository;

import java.util.List;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;

import com.gn.mvc.entity.Board;

public interface BoardRepository extends JpaRepository<Board, Long>, JpaSpecificationExecutor<Board> {
	
//	3. Specification 사용 / 오버로딩 했다~ 이걸 서비스의 selectBoardAll 에서 썼다.
	List<Board> findAll(Specification<Board> spec);
	
	// 1. 메소드 네이밍. findBy + 필드명 + 키워드 형태
//	List<Board> findByBoardTitleContaining(String keyword);
//	List<Board> findByBoardContentContaining(String keyword);
//	List<Board> findByBoardTitleContainingOrBoardContentContaining(String titleKeyword, String contentkeyword);
	
	// 2. JPQL 이용
	// 엔티티를 기준으로 하는 거라서 * 대신 별칭 정한 b라고 한다.
	@Query(value="SELECT b FROM Board b WHERE b.boardTitle LIKE CONCAT('%',?1,'%')") // :을 쓰는 경우 매개변수 이름 맞춰줘야한다. 
//	@Query(value="SELECT b FROM Board b WHERE b.boardTitle LIKE CONCAT('%',?1,'%')")
	List<Board> findByTitleLike(String keyword);
	
	@Query(value="SELECT b FROM Board b WHERE b.boardContent LIKE CONCAT('%',?1,'%')")
	List<Board> findByContentLike(String keyword01);
	
	@Query(value="SELECT b FROM Board b WHERE b.boardTitle LIKE CONCAT('%',?1,'%') OR b.boardContent LIKE CONCAT('%',?2,'%')")
	List<Board> findByTitleOrContentLike(String title, String content);	
}

BoardSpecification 클래스 생성

package com.gn.mvc.specification;

import org.springframework.data.jpa.domain.Specification;

import com.gn.mvc.entity.Board;

//	3번 방법 Specification 사용 / BoardService와 같이 보자.
public class BoardSpecification {
	// 제목에 특정 문자열이 포함된 검색 조건
	public static Specification<Board> boardTitleContains(String keyword){
		// root = Board 엔티티
		// query = 쿼리문
		// criteriaBuilder = 쿼리 조건(=like)
		return (root, query, criteriaBuilder) -> 
			criteriaBuilder.like(root.get("boardTitle"), "%"+keyword+"%");
	}
	
	// 내용에 특정 문자열이 포함된 검색 조건
	public static Specification<Board> boardContentContains(String keyword){
		return (root, query, criteriaBuilder) ->
			criteriaBuilder.like(root.get("boardContent"), "%"+keyword+"%");
	}
}

참고

  • DATETIME은 날짜 정보를 문자형으로 저장하고, TIMESTAMP는 1970년 1월 1일 0시 0분을 기준으로 몇초가 지났는지 정보를 숫자형으로 가지고 있다.
  • 데이터베이스는 스네이크 케이스 선호. JPA는 반드시 카멜 케이스로 작성.
profile
함께 공부해요!

0개의 댓글