교육 (Today I Learned)/Hanaro

[Hanaro] 60일차 / Spring Boot (JPA, HttpSession, Logging)

Bay Im 2024. 4. 13. 12:25

JPA 데이터 저장소 ArrayList→ MySQL 변환 실습

  • Model 사용 프로젝트 변환
    • build.gradle- dependencies 추가
      • Spring Data JPA, Lombok, MySQL, H2, Spring Web, Spring Web Services, Thymeleaf
    • application.properties 추가
      • # thymeleaf
        spring.thymeleaf.cache=false
        
        # jpa
        spring.jpa.hibernate.ddl-auto=update
        spring.jpa.generate-ddl=false
        spring.jpa.show-sql=true
        spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
        
        # pretty sql format
        spring.jpa.properties.hibernate.format_sql=true
        spring.jpa.properties.hibernate.use_sql_comments=true
        
        # mysql
        spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
        spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8&serverTimezone=Asia/Seoul
        spring.datasource.username=root
        spring.datasource.password=root
        
        # logging
        logging.level.org.hibernate.type.description.sql.BasicBinder=trace
        logging.level.org.hibernate.SQL=debug
        # Hibernate 6.1.5 updated in springboot 3.x
        logging.level.org.hibernate.orm.jdbc.bind=trace
    • 전체 출력 (SELECT)
      • List<Product> productList = productRepository.findAll();
    • 추가 (INSERT)
      •     @PostMapping("/add-action")
            public String addProduct(@RequestParam String name,
                                     @RequestParam int price,
                                     @RequestParam LocalDate limitDate) {
                Product product = Product.builder()
                        .name(name)
                        .price(price)
                        .limitDate(limitDate)
                        .build();
                productRepository.save(product);
        
                return "redirect:/";
            }
    • 수정할 데이터 출력 (SELECT-WHERE)
      • Product product = productRepository.findById(id).orElse(null);
        • findById(id) 뒤에 orElse(null)을 적어야한다. (id는 Long 타입)
    • 수정 (UPDATE)
      •     @PostMapping("/edit-action")
            public String editProduct(@RequestParam Long id,
                                      @RequestParam String name,
                                      @RequestParam int price,
                                      @RequestParam LocalDate limitDate) {
                Product product = productRepository.findById(id).orElse(null);
                product.setName(name);
                product.setPrice(price);
                product.setLimitDate(limitDate);
                productRepository.save(product);
        
                return "redirect:/";
            }
    • 삭제 (DELETE)
      •     @GetMapping("/delete-action")
            public String deleteProduct(@RequestParam Long id) {
                Product product = productRepository.findById(id).orElse(null);
                productRepository.delete(product);
        
                return "redirect:/";
            }

 

 

HttpSession, BindingResult 사용한 로그인/로그아웃, 회원가입 구현

  • build.gradle- dependencies 추가
    • implementation 'org.springframework.boot:spring-boot-starter-validation’
  • 로그인 구현
    • Login 데이터 넣을 DTO 클래스 생성
      • 클래스에 어노테이션 주입
        • @Getter, @Setter, @NoArgsConstructor
      • id, password 멤버 변수 생성
        • 각 멤버변수에 어노테이션 주입
          • @NotBlank(message = “ ”)
            • 빈 칸일 때 출력할 메시지 내용 작성
          • @Size(min = n, max = m)
            • 받아올 아이디/비밀번호의 문자열 길이 범위 지정
      • 예시
        • @Getter
          @Setter
          @NoArgsConstructor
          public class MemberLoginDTO {
              @NotBlank(message = "userId에 null, 빈문자열, 스페이스문자만 넣을 수 없습니다. ")
              @Size(min = 4, max = 20)
              private String userId;
              @NotBlank(message = "userPw에 null, 빈문자열, 스페이스문자만 넣을 수 없습니다. ")
              @Size(min = 4, max = 20)
              private String userPw;
          }
    • 컨트롤러에서 로그인 처리 메소드 작성
      • 메소드에 어노테이션 주입
        • @PostMapping, @ReponseBody
      • 매개변수 추가
        • 위에서 만든 login DTO (@Valid, @ModelAtribute 어노테이션 주입)
        • BindingResult
        • HttpSession
      • 예시 (loginAction)
        •     @PostMapping("/loginAction")
              @ResponseBody
              public String loginAction(@Valid @ModelAttribute MemberLoginDTO dto,
                                        BindingResult bindingResult,
                                        HttpSession session){
                  if( bindingResult.hasErrors() ){
                      //바인딩 오류
                      //DTO에 설정한 message값을 가져온다.
                      String detail = bindingResult.getFieldError().getDefaultMessage();
                      //DTO에 유효성체크를 걸어놓은 어노테이션명을 가져온다.
                      String bindResultCode = bindingResult.getFieldError().getCode();
                      System.out.println( detail + ":" + bindResultCode);
          
                      return "<script>alert('" + detail +"'); history.back();</script>";
                  }
                  System.out.println(dto.getUserId());
                  System.out.println(dto.getUserPw());
          
                  //로그인 처리 로직
                  //1. 메시지 : "아이디가 없습니다"
                  //2.       : "암호가 맞지 않습니다."
                  Optional<MemberEntity> optional =
                          memberRepository.findByUserId(dto.getUserId());
                  if( !optional.isPresent() ){
                      return "<script>alert('아이디가 없습니다.'); history.back();</script>";
                  }
                  Optional<MemberEntity> optional2
                          = memberRepository.findByUserIdAndUserPw(dto.getUserId(), dto.getUserPw());
                  if( !optional2.isPresent() ){
                      return "<script>alert('암호가 맞지 않습니다.'); history.back();</script>";
                  }
                  //세션에 로그인 여부/로그인 아이디/로그인 권한 저장.
                  session.setAttribute("isLogin", true);
                  session.setAttribute("userId", optional2.get().getUserId());
                  session.setAttribute("userRole", optional2.get().getUserRole());
          
                  String userRole = optional2.get().getUserRole();
                  if(userRole.equals("ROLE_ADMIN") == true) {
                      return "<script>alert('관리자 로그인 성공'); location.href='/admin';</script>";
                  } else {
                      return "<script>alert('로그인 성공'); location.href='/';</script>";
                  }
              }
    • 레포지토리에 해당 아이디/비밀번호의 엔티티 찾는 코드 작성
      • JPA 혹은 nativeQuery로 작성
      • 예시
        •     @Query(value = "select * from member m where m.user_id = :param_user_id " +
                      "and m.user_pw = :param_user_pw", nativeQuery = true)
          
              Optional<MemberEntity> findByUserIdAndUserPw(
                      @Param("param_user_id") String userId,
                      @Param("param_user_pw") String userPw);
          
              @Query(value = "select * from member m where m.user_id = :userId",
                      nativeQuery = true)
              Optional<MemberEntity> findByUserId(String userId);
    • html에 위에서 만든 메소드 Form에 적용하기
      • 예시
        • <form action="/loginAction" method="post">
        • <button type="submit">로그인하기</button>
  • 로그아웃 구현
    • 컨트롤러에서 로그아웃 처리 메소드 작성
      • 메소드에 어노테이션 주입
        • @GetMapping, @ResponseBody
      • 매개변수 추가
        • HttpSession
      • 로그인 관련 값을 null로 set한 후, session.invalidate(); 로 세션 종료
      • 예시 (logoutAction)
        •     @GetMapping("/logoutAction")
              @ResponseBody
              public String logoutAction(HttpSession session) {
                  // 로그아웃 액션
                  session.setAttribute("isLogin", null);
                  session.setAttribute("userId", null);
                  session.setAttribute("userRole", null);
          
                  session.invalidate();  // 세션 종료(JSESSIONID 종료), 모든 속성 제거
          
                  return "<script>alert('로그아웃되었습니다.'); location.href='/';</script>";
              }
    • html에 위에서 만든 메소드 Form에 적용하기
      • 예시
        • <button type="button" onclick="location.href='/logoutAction'">로그아웃</button>
  • 회원가입 구현
    • 회원가입 데이터 넣을 DTO 클래스 생성
      • 클래스에 어노테이션 주입
        • @Getter, @Setter, @AllArgsConstructor, @NoArgsConstructor, @Builder
      • 회원가입 정보들 멤버 변수 생성
        • 각 멤버변수에 어노테이션 주입
          • @NotBlank(message = “ ”)
            • 빈 칸일 때 출력할 메시지 내용 작성
          • @Size(min = n, max = m)
            • 받아올 아이디/비밀번호의 문자열 길이 범위 지정
          • @DateTimeFormat(pattern = "yyyy-MM-dd")
      • 예시
        • @Getter
          @Setter
          @AllArgsConstructor
          @NoArgsConstructor
          @Builder
          public class MemberJoinDTO {
              private Long id;
          
              @Size(min = 4, max = 20, message = "userId는 4자이상 20자 이하입니다.")
              @NotBlank(message = "null, 빈문자열, 스페이스문자열만 넣을 수 없습니다.")
              private String userId;
          
              @Size(min = 4, max = 20, message = "암호는 4자이상 20자 이하입니다.")
              @NotBlank(message = "null, 빈문자열, 스페이스문자열만 넣을 수 없습니다.")
              private String userPw;
          
              @NotBlank(message = "null, 빈문자열, 스페이스문자열만 넣을 수 없습니다.")
              private String userName;
          
              @NotBlank(message = "null, 빈문자열, 스페이스문자열만 넣을 수 없습니다.")
              private String userRole;
          
              private String userAddress;  // 엔티티에 없는 변수도 추가 가능
          
              @DateTimeFormat(pattern = "yyyy-MM-dd")
              private LocalDate joinDate;
          
              // DTO를 save용 Entity로 변환해주는 메소드
              public MemberEntity toSaveEntity() {
                  return MemberEntity.builder()
                          .userId(userId)
                          .userPw(userPw)
                          .userName(userName)
                          .userRole(userRole)
                          .joinDate(joinDate)
                          .build();
              }
    • 컨트롤러에서 로그인 처리 메소드 작성
      • 메소드에 어노테이션 주입
        • @PostMapping, @ReponseBody
      • 매개변수 추가
        • 위에서 만든 login DTO (@Valid, @ModelAtribute 어노테이션 주입)
        • BindingResult
      • 예시 (joinAction)
        •     @PostMapping("/joinAction")
              @ResponseBody
              public String joinAction(@Valid @ModelAttribute MemberJoinDTO dto,
                                       BindingResult bindingResult) {
                  if( bindingResult.hasErrors() ){
                      //바인딩 오류
                      //DTO에 설정한 message값을 가져온다.
                      String detail = bindingResult.getFieldError().getDefaultMessage();
                      //DTO에 유효성체크를 걸어놓은 어노테이션명을 가져온다.
                      String bindResultCode = bindingResult.getFieldError().getCode();
                      System.out.println( detail + ":" + bindResultCode);
          
                      return "<script>alert('" + detail +"'); history.back();</script>";
                  }
          
                  System.out.println("name: " + dto.getUserName());
                  try {
                      MemberEntity memberEntity = dto.toSaveEntity();
                      memberRepository.save(memberEntity);
                  } catch (Exception e) {
                      e.printStackTrace();
                      System.out.println("회원가입 실패");
                      return "<script>alert('회원가입 실패');history.back();</script>";
                  }
          
                  return "<script>alert('회원가입 성공');location.href='/';</script>";
              }
    • html에 위에서 만든 메소드 Form에 적용하기
      • 예시
        • <form action="joinAction" method="post">
        • <button type="submit">회원가입</button>
  • 데이터 값 확인 어노테이션 종류
    • //@NotNull	Null 불가
      //@Null	Null만 입력 가능
      //@NotEmpty	Null, 빈 문자열 불가
      //@NotBlank	Null, 빈 문자열, 스페이스만 있는 문자열 불가
      //@Size(min=,max=)	문자열, 배열등의 크기가 만족하는가?
      //@Pattern(regex=)	정규식을 만족하는가?
      //@Max(숫자)	지정 값 이하인가?
      //@Min(숫자)	지정 값 이상인가
      //@Future	현재 보다 미래인가?
      //@Past	현재 보다 과거인가?
      //@Positive	양수만 가능
      //@PositiveOrZero	양수와 0만 가능
      //@Negative	음수만 가능
      //@NegativeOrZero	음수와 0만 가능
      //@Email	이메일 형식만 가능
      //@Digits(integer=, fraction = )	대상 수가 지정된 정수와 소수 자리 수 보다 작은가?
      //@DecimalMax(value=) 	지정된 값(실수) 이하인가?
      //@DecimalMin(value=)	지정된 값(실수) 이상인가?
      //@AssertFalse	false 인가?
      //@AssertTrue	true 인가?

 

Logger

  • Slf4j (Simple Logging Facade for Java)
    • 파사드 패턴을 이용한 로깅 인터페이스
    • Slf4j를 앞에 세워두고 어플리케이션과 로깅 프레임워크 간의 직접적인 결합도를 없애고 파사드만을 바라보게 하여 로깅 구현체가 어떤 식으로 변경되든 변화를 무시 혹은 최소화할 수 있다.
  • Spring - logging 툴 사용 이유
    • System.out.println()은 IO리소스를 많이 사용하여 시스템이 느려질 수 있다.
    • 로그를 파일로 저장하여 분석할 필요가 있기 때문에 사용
  • logging 툴 종류
    • commons-logging
      • Spring3에서 사용하는 logging 툴
    • log4j
      • 효율적인 메모리 관리로 많이 사용되었다.
    • logback
      • log4j보다 성능이 더 우수하여 최근에 많이 사용되고 있다.
  • SLF4J
    • logback을 사용하기 위한 인터페이스

 

 

Spring Boot Logging

  • SLF4J
    • 로깅에 대한 추상 레이어를 제공하는 인터페이스 모음
  • @Slf4j
    • 스프링 부트에서 로그를 남기는 어노테이션
  • 스프링부트 Logging 순서
    • build.gradle- dependencies 추가
      • testCompileOnly 'org.projectlombok:lombok' // 테스트 의존성 추가
      • testAnnotationProcessor 'org.projectlombok:lombok' // 테스트 의존성 추가
    • application.yml 파일 생성
      • logging:
          level:
            com:
              study:
                ex21logger: debug
          pattern:
            file: "[%d{HH:mm:ss.SSS}][%-5level][%logger.%method:line%line] - %msg%n"
            level: info
        
          file:
            name: logs/mylog.log
        
          logback:
            max-history: 30
            file-name-pattern: "logs/mylog.%d{yyyy-MM-dd}.%i"
    • test 폴더 안에 LogTest 클래스 생성
      • 클래스에 어노테이션 주입
        • @Slf4j
      • ApplicationTests 클래스 상속받기 (extends)
      • 메소드 생성
        • 메소드에 어노테이션 주입
          • @Test
        • log 코드 작성
      • 예시
        • @Slf4j
          public class LogTest extends Ex21LoggerApplicationTests {
              static int count = 0;
          
              @Test
              public void TestLogger(){
                  Class myClass = LogTest.class;
                  // Class 객체 - 클래스에 대한 정보(멤버함수,멤버변수, 생성자)을 담고 있음.
          
                  Logger logger = LoggerFactory.getLogger(LogTest.class);
                  logger.trace("trace 로깅 {}", count++);
                  logger.debug("debug 로깅 {}", count++);
                  logger.info("info 로깅 {}", count++);
                  logger.warn("warn 로깅 {}", count++);
                  logger.error("error 로깅 {}", count++);
              }
          
              @Test
              public void testSlf4j(){
                  log.trace("Slf4j trace 로깅 {}", count++);
                  log.debug("Slf4j debug 로깅 {}", count++);
                  log.info("Slf4j info 로깅 {}", count++);
                  log.warn("Slf4j warn 로깅 {}", count++);
                  log.error("Slf4j error 로깅 {}", count++);
              }
          }
    • logback.xml 파일 생성
      • <configuration>
            <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                <!-- encoders are assigned the type
                     ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
                <encoder>
                    <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
            </appender>
        
            <root level="info">
                <appender-ref ref="STDOUT" />
            </root>
        </configuration>
    • logback-spring.xml 파일 생성
      • <?xml version="1.0" encoding="UTF-8" ?>
        <!-- 60초마다 설정 파일의 변경을 확인하여 변경시 갱신 -->
        <configuration scan="true" scanPeriod="60 seconds">
            <!-- 로그 패턴에 색상 적용 %clr(pattern){color} https://logback.qos.ch/manual/layouts.html#coloring -->
            <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
        
            <!-- springProfile 태그를 사용하여 profile 별 property 값 설정 -->
            <springProfile name="local">
                <!-- local log file path -->
                <property name="LOG_PATH" value="/Users/username/Desktop/log"/>
            </springProfile>
            <springProfile name="dev">
                <!-- dev log file path -->
                <property name="LOG_PATH" value="/home/instance/log"/>
            </springProfile>
        
            <!-- Environment 내의 프로퍼티들을 개별적으로 설정 -->
            <springProperty scope="context" name="LOG_LEVEL" source="logging.level.root"/>
        
            <!-- log file name -->
            <property name="LOG_FILE_NAME" value="log"/>
            <!-- err log file name -->
            <property name="ERR_LOG_FILE_NAME" value="err_log"/>
            <!-- console log pattern -->
            <property name="CONSOLE_LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss}:%-3relative]  %clr(%-5level) %clr(${PID:-}){magenta} %clr(---){faint} %clr([%15.15thread]){faint} %clr(%-40.40logger{36}){cyan} %clr(:){faint} %msg%n"/>
            <!-- file log pattern -->
            <property name="FILE_LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss}:%-3relative] %-5level ${PID:-} --- [%15.15thread] %-40.40logger{36} : %msg%n"/>
        
            <!-- Console Appender -->
            <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
                <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
                    <pattern>${CONSOLE_LOG_PATTERN}</pattern>
                </encoder>
            </appender>
        
            <!-- File Appender -->
            <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <!-- 파일경로 설정 -->
                <file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
        
                <!-- 출력패턴 설정 -->
                <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
                    <pattern>${FILE_LOG_PATTERN}</pattern>
                </encoder>
        
                <!-- Rolling 정책 -->
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <!-- .gz .zip 등을 넣으면 자동 일자별 로그 파일 압축 -->
                    <fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}_%i.gz</fileNamePattern>
                    <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                        <!-- 파일당 최고 용량 kb, mb, gb -->
                        <maxFileSize>10KB</maxFileSize>
                    </timeBasedFileNamingAndTriggeringPolicy>
                    <!-- 일자별 로그 파일 최대 보관주기(~일), 해당 설정일 이상된 파일은 자동으로 제거 -->
                    <maxHistory>30</maxHistory>
                    <!-- 전체 파일 크기를 제어하며, 전체 크기 제한을 조과하면 가장 오래된 파일을 삭제 -->
                    <totalSizeCap>1GB</totalSizeCap>
                </rollingPolicy>
            </appender>
        
            <!-- 에러의 경우 파일에 로그 처리 -->
            <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <filter class="ch.qos.logback.classic.filter.LevelFilter">
                    <level>error</level>
                    <onMatch>ACCEPT</onMatch>
                    <onMismatch>DENY</onMismatch>
                </filter>
                <file>${LOG_PATH}/${ERR_LOG_FILE_NAME}.log</file>
                <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
                    <pattern>${FILE_LOG_PATTERN}</pattern>
                </encoder>
        
                <!-- Rolling 정책 -->
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <!-- .gz .zip 등을 넣으면 자동 일자별 로그파일 압축 -->
                    <fileNamePattern>${LOG_PATH}/${ERR_LOG_FILE_NAME}.%d{yyyy-MM-dd}_%i.log</fileNamePattern>
                    <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                        <!-- 파일당 최고 용량 kb, mb, gb -->
                        <maxFileSize>10KB</maxFileSize>
                    </timeBasedFileNamingAndTriggeringPolicy>
                    <!-- 일자별 로그 파일 최대 보관주기(~일), 해당 설정일 이상된 파일은 자동으로 제거 -->
                    <maxHistory>60</maxHistory>
                </rollingPolicy>
            </appender>
        
            <!-- root 레벨 설정 -->
            <root level="${LOG_LEVEL}">
                <appender-ref ref="CONSOLE"/>
                <appender-ref ref="FILE"/>
                <appender-ref ref="ERROR"/>
            </root>
        </configuration>
728x90