728x90

개요

제가 디버거 모드를 이용하면서 Step Into, Resume 등 헷갈리는 기능도 있고 디버거를 잘 활용하고 싶어 작성하게 되었습니다. 또한 디버거 이용하는 방법에 잘 모르는 개발자 분들을 위해 작성하게 되었습니다.

1. Break Point

47번 줄, 48번 줄을 보면 빨간색 점이 찍혀있는 것을 볼 수 있다. 이는 Break point로 디버거 모드로 실행시켰을 때 코드 실행이 멈추는 지점

2. 디버거 모드로 실행시키는 방법

프로젝트 화면 상단에 실행시키는 버튼 옆에 벌레같이 생긴 버튼을 누르면 디버거 모드로 실행된다.

3. Step Over

Step Over는 코드 다음 줄로 넘어간다. method call 안으로 들어가지 않는다.

4. Step Into

지금 대기하고 있는 method call 내부로 들어간다.

Step Into를 누르면 아래의 화면과 같이 색이 나오는데 이는 색이 나오는 곳 중 어디로 들어갈 것인지 나타내는 색이다.

어떤 method call 안으로 들어갈 것인지 선택하면 된다.

5. Step Out

Step Into의 반대 개념으로 들어간 스택에서 나오는 기능이다. 근데 나오는데 그 스택을 실행을 시키고 빠져나온다.

6. Drop Frame

Step Out과 유사한 기능으로 그 스택을 실행시키지 않고 빠져나온다.

7. Resume Program

다음 Break Point로 이동한다.

8. Run to Cursor

Break Point를 찍고 Resume 버튼으로 다음 Break Point로 이동을 하였는데 Run to Cursor를 이용하면 Break Point를 찍지 않고 커서를 이용해 커서를 클릭하는 위치로 이동한다.

9. Condition

예를 들어 Step Into를 사용하면 for문이 50번까지 돌 때 49번째의 값을 확인하고 싶으면 Step Into를 49번 실행시켜야 한다. 하지만 Break Point에 우클릭을 해서 Condition에 확인하고 싶은 값의 Boolean 형태로 값을 넣는다. 그러면 해당하는 for문의 값만 실행시킬 수 있다.

10. Evaluate Expression

멈춘 지점에서 이런저런 값을 계산해 볼 수 있다.

Code fragment에서 실행하고 싶은 코드를 작성하고 Evaluate를 누르면 실제 콘솔에서 실행된 것을 확인할 수 있다.

728x90
728x90

로깅이란?

프로그램 동작시 발생하는 모든 일을 기록하는 행위이다. 모든 일이란 서비스 동작 상태와 장애로 나눌 수 있는데 서비스 동작 상태에는 시스템 로딩, HTTP 통신, 트렌젝션, DB 요청, 의도를 가진 Exception 등이 있고 장애(exception, error)로는 I/O Exception, NullPointException, 의도하지 않은 Exception 등이 있다.

로깅은 언제 할까?

프로젝트 성격에 맞게, 팀에 맞게 진행한다. 따라서 로깅 시점은 때에 따라 다르다.

로깅을 어떻게 해?

  1. System.out.println("로깅로깅")
  2. System.err.println("에러로깅")
  3. 로깅 프레임워크를 사용

로깅 프레임워크

  1. SLF4J
  2. Logback
  3. Lof4j
  4. nlog 등

로깅 vs System.out.println()

  1. 출력 형식을 지정할 수 있음
  2. 로그 레벨에 따라 남기고 싶은 로그를 별도로 지정할 수 있음
  3. 콘솔뿐만 아니라 파일이나, 네트워크 등 로그를 별도에 위치에 남길 수 있다.
  4. log 성능이 System.out 보다도 좋다고 한다.

로그 레벨 관련

Logback 은 5단계의 로그 레벨을 가진다.

심각도 수준은 Error > Warn > Info > Debug > Trace이다.

레벨 설명
Fatal 매우 심각한 에러, 프로그램이 종료되는경우가 많아 거의 사용되지 않음(logback 에는 fatal 설정이 아예 존재하지 않고, error 에 맵핑됩니다.)
Error 의도하지 않은 에러가 발생한 경우프로그램이 종료되지 않음프로그램 내에서 개발자가 의도하지 않은 예외를 나타날 때 사용
Warn 에러가 될 수 있는 잠재적 가능성이 있는 경우Warn 로그가 발생했을 시, 알람을 통해 개발자가 크리티컬한 에러를 맞닥뜨리기 전에 확인할 수 있는 역할을 겸함
Info 명확한 의도가  있는 정보성 로그요구사항에 따라 시스템 동작을 보여줄 떄
Debug Info 레벨보다 더 자세한 정보가 필요한 경우주로 Develop 환경에서 사용
Trace Debug 레벨보다 더 자세한 내용을 포함Dev 환경에서 버그를 해결하기위해 사용최종 프로덕션이나 커밋에 포함되면 안된다고 합니다.

회원가입 시, DB에 동일한 email을 가진 회원이 있을 때, DuplicationException을 던진다면 이 이벤트의 로그는 어떤 레벨을 적용할까? → Info

로깅 vs 디버깅

  1. 프로그래밍의 절반은 디버깅이다.
  2. 디버깅할 수 없는 상황에서는 로깅이 최선의 선택
  3. 디버깅을 쓸 수 있다면 디버깅을 최대한 활용

SLF4J

Simple Logging Facade for Java

다양한 로깅 프레임 워크(java.util.logging, logback 및 log4j)에 대한 추상화(인터페이스) 역할을 하는 라이브러리입니다.

인터페이스이기 때문에 단독으로 사용이 불가능합니다.

최종 사용자가 배포 시 원하는 구현체(logback, log4j 등)를 선택하여 사용 가능하다.

SLF4J 동작 과정

개발할 때, SLF4J API를 사용하여 로깅 코드를 작성

배포할 때, 바인딩된 Logging Framework가 실제 로깅 코드를 수행

Bridge

다른 로깅(SLF4J 이외의)의 API로의 Logger 호출을 SLF4J 인터페이스로 연결(redirect)하여 SLF4J API가 대신 처리할 수 있도록 하는 라이브러리이다.

이전의 레거시 로깅 프레임워크를 위한 라이브러리이다.

Bridge는 여러 개를 사용해도 상관없지만, 사용 시 주의점은 Bridge와 Binder에 같은 종류의 프레임워크를 사용하면 안 된다.

SLF4J API(인터페이스)

로깅에 대한 추상 레이어(인터페이스)를 제공한다. 즉 로깅 동작에 대한 역할을 수행할 추상 메서드를 제공한다. 앞서 말했듯이 추상 클래스이기 때문에 이 라이브러리만 단독적으로 쓰일 수 없다. 사용 시 주의점은 반드시 하나의 API에 하나의 Binding을 둬야 한다.

SLF4J Binding(.jar)

SLF4J 인터페이스를 로깅 구현체(Logging Framework)와 연결하는 어댑터 역할을 하는 라이브러리이다. SLF4J API를 구현한 클래스에서 Binding으로 연결될 Logger의 API를 호출해요. SLF4J API의 주의점과 같이 하나에 API에 하나의 Binding을 둬야 한다.

SLF4J 실습

build.gradle에 아래와 같은 의존성을 추가한다.

// slf4j & logback
implementation 'org.slf4j:jcl-over-slf4j'
implementation 'ch.qos.logback:logback-classic'

@slf4j 어노테이션을 사용해도 된다.

slf4j는 기본적으로 debug가 default 값으로 잡혀있어 trace 로그를 띄어주기 위해서는 별도의 설정이 필요하다고 합니다.

logging:
  level:
    org:
      springframework: trace

application.yml 파일에 위의 코드를 추가해주어야 한다.

Logback

logback은 로깅 프레임워크 중 하나로 SLF4J의 구현체 라이브러리이다.

Log4J를 토대로 만든 프레임워크로 현재 Spring framework에서도 Slf4j 와 logback을 기본 라이브러리로 채택하고 있다.

Logback 구조

logback-core

logback-classic, logback-access 두 모듈의 기반 역할을 하는 모듈이며 Appender, Layout 인터페이스가 이 모듈에 속해 있습니다.

logback-classic

logback-core를 가지며 slf4j API를 구현하여, log4j 또는 java.util.logging(JUL)과 같은 다른 로깅 프레임워크 간에 쉽게 전환 가능하도록 하며 Logger 클래스가 이 모듈에 속해 있습니다.

logback-access

Servlet Container와 통합되어 Tomcat 및 jetty 등 Servket 컨테이너에 HTTP Access 로그 기능을 제공합니다. 웹 애플리케이션 레벨이 아닌 컨테이너 레벨에서 설치되어야 합니다.

logback vs logback-spring

스프링 공식문서를 살펴보면 가능하다면 표준보다 "-spring"을 붙여서 사용하는 걸 추천한다고 합니다.

표준 버전 즉, logback.xml을 사용한다면 Spring 이 완벽하게 로그 초기화를 제어하지 못한다고 한다.

아마도, logback.xml 파일은 너무 빨리 로드되기 때문에, 이 파일 안에선 Extensions을 사용할 수 없다고 하는데 이 때문인 것 같습니다.

로그 백 설정 파일은 logback-spring.xml 파일을 src/main/resources/logback-spring.xml 경로에 만들면서 시작합니다.

로그 파일 작성하기

콘솔 로그의 수준을 변경하는 방법은 application.yml과 logback-spring.xml 에서 설정하는 방법이 있다. application.yml 은 설정하는 난이도가 비교적 쉽지만, 실제 제품에 사용하기엔 한계가 있고 세부적인 설정이 불편하기 때문에 logback-spring.xml로 관리하는 편이 더 좋다고 한다.

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--  여기애 로그 설정 부분이 들어간다.  -->
</configuration>

logback-spring.xml 구성 요소

  1. appender는 로그가 출력되는 위치를 나타냅니다.
    1. XXXAppender로 끝나는 클래스들이 존재하며 다양한 로그 출력 위치 및 방법을 제공합니다.
    2. ConsoleAppender는 콘솔에 System.out 또는 System.err를 이용하여 로그 이벤트를 append 합니다.
    3. FileAppender는 파일에 로그를 저장한다. 최대 보관 일 수 등을 지정할 수 있다.
    4. RollingFileAppender : 여러 개의 파일을 롤링, 순회하면서 로그를 찍는다.(FileAppender를 상속받는다. 지정 용량이 넘어간 Log File을 넘버링하여 나누어 저장할 수 있다.)
    5. SMTPAppender : 로그를 메일로 보낸다.
    6. DBAppender: DB(데이터베이스)에 로그를 쌓는다.
  2. layout (encoder)
    1. logback 버전 0.9.19 이후 에로 로그의 모든 wirte 권한을 제어할 수 있는 encoder 등장하였습니다.
    2. Encoder는 로그 이벤트를 바이트 배열로 변환하고, 해당 바이트 배열을 OutputStream에 쓰이는 작업을 담당합니다.
    3. 이전 버전에서 appender는 이벤트 메시지를 문자열로 변환하는데 layout을, write 하는데 java.io.Writer를 사용해 왔습니다.
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
      <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
          <pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{0}) - %msg%n
          </pattern>
        </encoder>
      </appender>
    </configuration>
    
    append name설정은 STDOUT이라는 변수명으로 저장해뒀다고 생각하면 된다.
  3. springProfile
    1. 스프링 배포 버전 profile에 따라서 로그 설정을 세분화할 수도 있습니다.
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
      <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
          <pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{0}) - %msg%n
          </pattern>
        </encoder>
      </appender>
    
      <property name="LOG_DIR" value="/var/log/was"/>
    
      <springProfile name="prod">
        <appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="FILE">
          <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %logger{0} - %msg%n</pattern>
          </encoder>
          <file>${LOG_DIR}/app.log</file>
    
          <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_DIR}-%d{yyyy-MM-dd}-%i-log.zip</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>90</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
          </rollingPolicy>
        </appender>
      </springProfile>
    
      <root level="info"> <!-- info 레벨 이상에서만 실행한다. -->
        <springProfile name="dev">
          <appender-ref ref="STDOUT"/>
        </springProfile>
        <springProfile name="prod">
          <appender-ref ref="STDOUT"/>
          <appender-ref ref="FILE"/>
        </springProfile>
      </root>
    </configuration>
    
    이렇게 하면 prod 버전의 배포 환경일 때만, 그 하위 내용들이 적용되며, appender 또한 해당 profile이 아닐 때는 무시됩니다.
    1. root 태그를 이용해 만들어둔 appender 들을 조합해서 사용할 수 있습니다. <root level = "off">를 하게 된다면 모든 로거가 무시됩니다.
    2. <springProfile> 속성을 이용해 배포 버전에 마다 다른 로거 설정을 세분화할 수 있습니다.
    3. <appneder-ref>를 이용해 만들어둔 appender를 여기서 마치 변수처럼 사용할 수 있습니다.

오류

만약 프로젝트 실행을 했는데 아래와 같은 오류가 발생하면

파일을 저장하는 경로에 value=”/var/log/was”에 value=”./var/log/was”로 수정해주면 정상적으로 작동한다.

Appender 코드

   1. ConsoleAppender

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{0}) - %msg%n
      </pattern>
    </encoder>
  </appender>
</configuration>

   2. RollingFileAppender

<property name="LOG_DIR" value="/var/log/was"/>
<springProfile name="prod">
    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="FILE">
      <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %logger{0} - %msg%n</pattern>
      </encoder>

      <file>${LOG_DIR}/app.log</file> <!-- 파일을 저장할 경로를 정한다 -->
			<filter class="ch.qos.logback.classic.filter.LevelFilter">
	      <level>INFO</level>
		    <onMatch>ACCEPT</onMatch> <!-- 해당 레벨만 기록한다. -->
		    <onMismatch>DENY</onMismatch> <!-- 다른 수준의 레벨은 기록하지 않는다.(상위 레벨도 기록 안함), 상위 수준의 레벨에 대한 기록을 원하면 ACCEPT 로 하면 기록된다. -->
		  </filter> <!-- 레벨별 필터링이 필요없을 경우 filter class 관련된 부분을 삭제하면 됨-->
      
			<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>${LOG_DIR}-%d{yyyy-MM-dd}-%i-log.zip</fileNamePattern>
        <maxFileSize>100MB</maxFileSize> <!-- 한 파일의 최대 용량(분할 용량) -->
        <maxHistory>90</maxHistory> <!-- 한 파일의 최대 저장 기한 -->
        <totalSizeCap>3GB</totalSizeCap> <!-- 전체 파일의 크기를 제어하며, 전체 크기 제한을 초과하면 가장 오래된파일은 삭제한다. totalSizeCap을 사용하려면 maxHistory가 필수 속성이다. -->
      </rollingPolicy>
    </appender>
  </springProfile>
  • maxFileSize: 한 파일의 최대 용량(분할 용량)이다.
  • maxHistory: 한 파일의 최대 저장 기한 롤 오버 지정에 따라 maxHistory 기간이 정해진다.
  • totalSizeCap: 전체 파일의 크기를 제어하며, 전체 크기 제한을 초과하면 가장 오래된 파일은 삭제한다. totalSizeCap을 사용하려면 maxHistory가 필수 속성이다.

   3. SMTPAppender

<appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
    <smtpHost>ADDRESS-OF-YOUR-SMTP-HOST</smtpHost>
    <to>EMAIL-DESTINATION</to>
    <to>ANOTHER_EMAIL_DESTINATION</to> <!-- additional destinations are possible -->
    <from>SENDER-EMAIL</from>
    <subject>TESTING: %logger{20} - %m</subject>
    <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%date %-5level %logger{35} - %message%n</pattern>
    </layout>       
</appender>

   4. DBAppender

<property resource="application.properties" />
<springProperty name="spring.datasource.driverClassName" source="spring.datasource.driverClassName"/>
<springProperty name="spring.datasource.url" source="spring.datasource.url"/>
<springProperty name="spring.datasource.username" source="spring.datasource.username"/>
<springProperty name="spring.datasource.password" source="spring.datasource.password"/>

<appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
    <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
        <driverClass>${spring.datasource.driverClassName}</driverClass>
        <url>${spring.datasource.url}</url>
        <user>${spring.datasource.username}</user>
        <password>${spring.datasource.password}</password>
    </connectionSource>
</appender>

DBAppender 사용 시 DB 필요한 Table

Logback에서 DBAppender는 독립적인 형식으로 logging_event, logging_event_property, logging_event_exception 세 개의 데이터 베이스 테이블을 insert 하므로 DBAppender를 사용하기 전에 먼저 테이블을 생성해야 합니다.

DROP TABLE IF EXISTS logging_event_property;
DROP TABLE IF EXISTS logging_event_exception;
DROP TABLE IF EXISTS logging_event;

CREATE TABLE logging_event
(
    timestmp         BIGINT NOT NULL,
    formatted_message  TEXT NOT NULL,
    logger_name       VARCHAR(254) NOT NULL,
    level_string      VARCHAR(254) NOT NULL,
    thread_name       VARCHAR(254),
    reference_flag    SMALLINT,
    arg0              VARCHAR(254),
    arg1              VARCHAR(254),
    arg2              VARCHAR(254),
    arg3              VARCHAR(254),
    caller_filename   VARCHAR(254) NOT NULL,
    caller_class      VARCHAR(254) NOT NULL,
    caller_method     VARCHAR(254) NOT NULL,
    caller_line       CHAR(4) NOT NULL,
    event_id          BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
);

CREATE TABLE logging_event_property
(
    event_id          BIGINT NOT NULL,
    mapped_key        VARCHAR(254) NOT NULL,
    mapped_value      TEXT,
    PRIMARY KEY(event_id, mapped_key),
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);

CREATE TABLE logging_event_exception
(
    event_id         BIGINT NOT NULL,
    i                SMALLINT NOT NULL,
    trace_line       VARCHAR(254) NOT NULL,
    PRIMARY KEY(event_id, i),
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);

DBAppender와 테이블을 생성했다면 해당 테이블에 로그가 자동으로 insert 됩니다.

 

참조

https://tecoble.techcourse.co.kr/post/2021-08-07-logback-tutorial/

 

Logback 으로 쉽고 편리하게 로그 관리를 해볼까요? ⚙️

Spring Boot…

tecoble.techcourse.co.kr

https://thalals.tistory.com/373

 

[Spring] 로그 프레임워크와 로그백이란 - 로깅에 대해 알아보자

이번 포스팅은, CS 요청건 처리 시, 과거 이력을 확인해야할 때, 로그를 효율적으로 남기지 못해 명확한 결과를 전달하지 못한 아쉬운 경험을 토대로 Spring Boot 를 이용하여 로깅을 설정해보기위

thalals.tistory.com

https://oingdaddy.tistory.com/317

 

Springboot Logging (Logback) application.yml 에서 하기

필자는 xml로 설정하는 방식이 익숙한 사람이었는데 점점 이런 xml 파일로 설정하는 부분들이 없어지고 이는 다른 부분으로 대체가 되고 있다. java config라던지 application.yml 파일에 기존에 xml로 설

oingdaddy.tistory.com

https://reference-m1.tistory.com/349#recentEntries

 

[Back end] logback 시간, 용량 Rolling 적용

logback.qos.ch/manual/appenders.html Chapter 4: Appenders Chapter 4: Appenders 和訳 (Japanese translation) There is so much to tell about the Western country in that day that it is hard to know wher..

reference-m1.tistory.com

https://www.youtube.com/watch?v=1MD5xbwznlI
https://www.youtube.com/watch?v=JqZzy7RyudI&t=16s

 

728x90
728x90

RedirectAttributes에서 addAttribute와 addFlashAttribute 차이가 있습니다.

 

1. addAttribute

addAttribute는 보통 해당 페이지로 리다이렉트를 할 때 값을 넘겨주는 용도로 사용한다.

 

addAttribute를 사용하면 URL 뒤에 붙게돼서 addFlashAttribute와는 달리 값이 유지가 됩니다.

위 사진과 같이 URL에 result의 값이 전달됩니다. 

 

2. addFlashAttribute

addFlashAttribute를 사용해서 리다이렉트를 실행하면

addFlashAttribute로 전달한 result는 URL에 존재하지 않는 것을 확인할 수 있다.

 

즉, addAttribute는 URL에 붙어서 값이 전달되어 유지가 되지만 addFlashAttribute는 일회성으로 URL에 붙지 않고 세션 후 재지정 요청이 들어오면 값은 사라지게 됩니다. 즉 addFlashAttribute는 휘발성 성질을 가지고 있습니다. 성공 여부를 가리기 위해 임시적으로 값을 전달할 때 addFlashAttribute를 이용하면 됩니다.

728x90
728x90

HttpServletRequest는 Http Servlet에 대한 요청 정보를 제공하도록 ServletRequest를 확장한 인터페이스이다.

HttpServletRequest는 서블릿 컨테이너가 생성하며 서블릿의 service() 메서드의 매개변수로 보냅니다.

서블릿의 생명주기(Life Cycle)

1. 서블릿 컨테이너가 서블릿 인스턴스의 init() 메서드를 호출하여 초기화한다.

  • 최초 요청시 한 번만 초기화되며 그 이후로는 이 과정을 생략한다.

2. 서블릿이 초기화된 다음부터 클라이언트가 요청을 처리할 수 있다. 각 요청은 별도의 스레드로 처리하고 이때 서블릿의 service() 메서드를 호출한다.

  • 이 안에서 HTTP 요청을 받고 클라이언트로 보낼 HTTP 응답을 만든다.
  • service()는 Http Method에 따라 doGet() 또는 doPost() 등으로 위임하여 처리한다.

3. 서블릿 컨테이너 판단에 따라 서블릿을 메모리에서 내려야 할 시점에 destroy() 를 호출한다.

public class HelloServlet extends HttpServlet {
  @Override
  public void init() throws ServletExcetion {
    System.out.println("init");
  }
  @Override
  public void doGet(HttpServletReqeust req, HttpServletResponse res) throws ServletExcetion {
    System.out.println("doGet");
  }
  @Override
  public void destory() {
    System.out.println("destroy");
  }
}

서블릿(servlet)은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱 합니다. 그리고 그 결과를 HttpServletRequest 객체에 담아서 제공합니다. 서블릿 덕분에 개발자들은 HTTP 스펙을 편리하게 사용할 수 있습니다. HttpServletRequest의 핵심 기능은 HttpServlet의 요청을 받아서 꺼내서 쓸 수 있다는 것입니다. 그럼 이제 HttpServletRequest로 어떻게 컨트롤러에서 값을 꺼내는지 알아보겠습니다.

<form id='actionForm' action="/board/user" method='post'>
  <input type="text" name="name" value="lusida" />
  <input type="text" name="age" value="26" />
</form>

위처럼 JSP에서 사람 정보를 입력하고 POST로 넘기면 formData 형식처럼 key와 value의 값으로 HttpServletRequest에 담아서 컨트롤러로 전달합니다.

  • content-type : application/x-www/form-urlencoded
  • 메시지 바디에 쿼리 파라미터 형식으로 전달 username=lusida&age=26

GET의 경우 URL 뒤에 /board/user?name=lusida&age=26 형식으로 전달되는데 물음표(?) 뒤의 문자열들을 쿼리스트링 또는 요청 파라미터라고 부릅니다. GET 방식의 경우에도 ?key=value&key=value 형식으로 HttpServletRequest에 담아서 컨트롤러로 전달합니다.

@PostMapping("/user")
public String user(HttpServletRequest request) {
  String name = request.getParameter("name"); // key 값을 이용해서 꺼내올 수 있다.
  String age = request.getParameter("age"); // key 값은 input 에서 설정한 name 값이다.
  
  // 만약에 JSP 에서 설정한 name="userName" 이라는 키값이 여러개인 경우에는
  // getParameterValues() 메서드를 이용하여 배열로 받아올 수 있다.
  String[] names = request.getParameterValues("name");
  
  return REDIRECT_LIST;
}

중요 : key 값은 input에서 설정한 name 값이다.

 

@RequestParam

HttpServletRequest과 동일하게 @RequestParam 은 1:1 방식입니다. 차이점은 HttpServletRequest 대신 @RequestParam이라는 어노테이션을 사용한다는 점입니다.

<form id='actionForm' action="/board/user" method='post'>
  <input type="text" name="name" value="lusida" />
  <input type="text" name="age" value="26" />
</form>
@PostMapping("/user")
public String user(@RequestParam String name, @RequestParam String age) {
  // @RequestParam 뒤에 붙는 매개변수 변수명은 JSP 에서 설정한 name 의 key 값과 동일해야 한다.
  return REDIRECT_LIST;
}

HttpServletRequest와 @RequestParam을 이용하여 받아오는 경우 요청 파라미터가 많아지면 많아질수록 컨트롤러 내부 코드나 매개변수가 증가하여 코드 가독성이 떨어지고, 작성되는 코드 양이 많아집니다. 이러한 문제를 해결하고자 나온 것이커맨드 객체(Command Object)입니다.

 

커맨드 객체

HttpServletRequest를 통해 들어온 요청 파라미터들을 setter 메서드를 이용하여 객체에 정의되어있는 속성에 바인딩이 되는 객체를 의미한다. 커맨드 객체는 보통 VO 나 DTO를 의미하며, HttpServletRequest로 받아오는 요청 파라미터의 key 값과 동일한 이름의 속성들과 setter 메서드를 가지고 있어야 합니다. 

 

어떻게 자동으로 바인딩을 시켜주냐 하면, 스프링이 내부적으로 HttpServletRequest와 커맨드 객체의 setter 메서드를 이용하여 알아서 바인딩시켜줍니다.

@Getter @Setter
public class User {
  private String name;
  private String age;
}
@PostMapping("/user")
public String user(User user, Model model) {
  String name = user.getName();
  String age = user.getAge();
  
  // user 파라미터를 model 에 담는다.
  model.addAttribute("user", user);
  return REDIRECT_LIST;
}

위에서 user 파라미터를 model에 담는 것을 볼 수 있습니다. 이 코드 또한 @ModelAttribute 어노테이션을 사용하여 제거할 수 있습니다.

 

커맨드 객체의 역할

  1. 컨트롤러에서 View로 바인딩 : View 단에서 form:form 태그를 사용하는 경우
  2. View에서 컨트롤러로 바인딩 : View 단에서 input type="text" 혹은 input type="hidden"으로 값을 컨트롤러로 전송하는 경우
  3. 컨트롤러에서 Mapper.xml로 바인딩 : Mapper.xml 에서 title = #{title}, contents = #{contents}처럼 사용하는 경우, 커맨드 객체를 통해 #{변수명}과 커맨드 객체의 필드명을 통해 바인딩해주는 경우

 

@ModelAttribute 

@ModelAttribute는 크게 메서드명 위에 사용되는 경우와 파라미터 옆에 사용되는 경우로 나뉩니다.

@ModelAttribute는 커맨드 객체와 같이 요청 파라미터들을 객체 프로퍼티에 바인딩시켜준다는 것입니다. 하지만 @ModelAttribute를 생략해도 커맨드 객체를 이용해서 바인딩이 되는데, @RequestParam 또한 생략해도 사실상 바인딩이 가능합니다. 그 이유는 스프링이 내부적으로 String이나 int 등은 @RequestParam으로 보고, 그 외의 복잡한 객체들은 @ModelAttribute 가 생략됐다고 간주하기 때문입니다. 하지만 그렇다고 무조건 생략하는 것은 위험합니다.

@PostMapping("/ins")
public String ins(@ModelAttribute User user, Model model) {
  String name = user.getUserName();
  String age = user.getAge();
  
  // user 객체를 모델에 담는 코드를 작성하지 않아도, 담겨져 있다.
  // 내부적으로 model.addAttribute("user", user); 로 담는다.
  // 만약 객체명과 변수명이 @ModelAttribute UserVo user 로 되어있는 경우에는 어떻게 담길까?
  // 클래스명을 기준으로 카멜케이스를 적용하여 model.addAttribute("userVo", user); 로 담는다.
  
  return REDIRECT_LIST;
}

@ModelAttribute의 역할 중 하나는 model 에 객체를 담아준다는 것입니다. 파라미터 객체 옆에 @ModelAttribute를 사용했을 때 얻는 또 다른 이점은 @ModelAttribute 가 붙은 파라미터를 처리할 때는 @RequestParam과 달리 검증(Validation) 작업을 내부적으로 진행합니다.

@RequestParam의 경우 스프링의 기본 타입 변환 기능을 이용해서 요청 파라미터 값을 메서드 파라미터 타입으로 변환하는데, 만약 숫자 타입의 파라미터라면 문자열 타입으로 들어온 요청 파라미터의 타입 변환을 시도하고 실패하면 Http 400 Bad Request 응답이 클라이언트로 가게 됩니다.

하지만 @ModelAttribute의 경우 내부적으로 검증(Validation) 작업을 진행하기 때문에 setter 메서드를 이용하여 값을 바인딩하려고 시도하다가 예외를 만나지만 작업이 중단되면서 Http 400 Bad Request 가 발생하지는 않습니다. 타입 변환에 실패해도 작업은 계속되며 BindingException 타입의 객체에 담겨서 컨트롤러로 전달됩니다. 보통 등록이나, 수정을 처리하는 핸들러 메서드의 경우 다양한 검증을 실시해야 하고, 사용자의 입력 값에 오류가 있을 때에는 이에 대한 처리를 컨트롤러에게 맡겨야 합니다.

따라서 @ModelAttribute를 통해서 폼의 정보를 전달받는 경우 Errors 객체나 BindingResult 객체를 @ModelAttribute 가 붙은 파라미터 바로 뒤에 선언해서 검증 처리를 실시합니다. Errors 나 BindingResult는 자신의 바로 앞에 있는 파라미터 검증에서 발생한 오류들만 전달해주기 때문에 @Valid 나 @Validated, @ModelAttribute 가 붙은 파라미터 바로 뒤에 선언되어야 합니다.

 

메서드 위에 @ModelAttribute 가 사용되는 경우

컨트롤러에서 메서드 위에 @ModelAttribute 가 사용되는 경우는, 해당 컨트롤러 내의 어떠한 핸들러 메서드들보다 먼저 동작하게 됩니다.

/**
 * @ModelAttribute 메서드가 먼저 동작하기 때문에,
 * 다른 핸들러 메서드에서 model 에 담겨져있는 user 키값을 이용하여 user 객체를 꺼내서 쓸 수 있다.
 */
@ModelAttribute("user")
public String initUser() {
  // 내부적으로 model.addAttribute("user", userService.findUser(FIRST_USER_SEQ)); 형태로 담는다.
  return userService.findUser(FIRST_USER_SEQ); 
}

따라서 여러 핸들러 메서드에서 공통으로 쓰이며, View 단에서도 꺼내 쓸 일이 있는 것들은 이런 식으로 처리해서 사용하기도 합니다.

 

참고 블로그

https://medium.com/webeveloper/modelattribute-%EC%99%80-%EC%BB%A4%EB%A7%A8%EB%93%9C-%EA%B0%9D%EC%B2%B4-command-object-42c14f268324

 

@ModelAttribute 와 커맨드 객체(Command Object)

@ModelAttribute 와 커맨드 객체(Command Object), @RequestParam 과 HttpServletRequest

medium.com

 

728x90

+ Recent posts