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

예외란?

예외란 프로그램 실행 중 발생하는 이벤트로 프로그램 명령의 정상적인 흐름을 방해하는 것이다. 예외가 발생하게 될 경우 예외객체를 만들어 런타임 시스템에 전달한다. 예외객체는 오류발생 당시의 프로그램 상태와 오류정보가 포함되어있다. 이러한 과정을 예외 발생이라고 한다.

예외 요청 흐름

WAS는 스프링 부트가 등록한 에러 설정(/error)에 맞게 요청을 전달하는데, 이러한 흐름을 총 정리하면 다음과 같다.

WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러
-> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣)
-> WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러(BasicErrorController)

컨트롤러 하위에서 예외가 발생하였을 때, 별도의 예외 처리를 하지 않으면 WAS까지 에러가 전달된다. 그러면 WAS는 애플리케이션에서 처리를 못하는 예와라 exception이 올라왔다고 판단을 하고, 대응 작업을 진행한다.

Exception의 종류

자바에서의 예외는 전부 Throwable을 상속받는다.

Error의 경우는 개발자가 직접 사용할 일은 거의없다. 자바가 JVM이나 내부에 있는 라이브러리에서 문제가 될법한 상황에서 나온다. 즉, 애플리케이션이 정상적으로 동작하는데 심각한 문제가 있는 경우이다.

우리가 주로 사용하게 되는 에러는 Exception과 RuntimeException이다.

우리가 사용하는 예외는 크게 Checked Exception, Unchecked Exception 2종류로 분류 할 수 있다.

Exception -> Checked Exception
RuntimeException -> Unchecked Exception

Checked Exception

컴파일 시점에서 Exception을 catch하는지 검사하기 때문에 처리를 하지 않을경우 컴파일 에러가 발생한다. 현 위치에서 처리를 할경우 try/catch를 하여 해결한다. 현 위치에서 처리를 하지 않을 경우 throw 방식을 이용해여 나를 호출한 상위에 위임한다.

Unchecked Exception

Exception이 발생하는 메소드에서 throws 예약어를 이용하여 Exception을 호출 메소드에 전달해야 한다.

Runtime Time Exception 이라고 한다. 컴파일 시점에 Exception을 catch하는지 확인하지 않기 때문에 컴파일 시점에 Exception이 발생할 것인지의 여부를 판단할 수 없다. Exception이 발생하는 메소드에서 throws 예약어를 활용해 Exception을 처리할 필요가 없다. 하지만 처리해도 무방하다.

try,catch와 throw

예외 복구 : 예외가 발생하면 예외 상황에 대해 알맞게 처리하여 복구한다. ex) try, catch

예외 회피 : 예외를 직접 처리하지 않고 예외를 상위 메소드에 위임한다. ex) throw

스프링이 제공하는 다양한 예외처리 방법

Spring은 에러 처리라는 공통 관심사(cross-cutting concerns)를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안하였고, 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다. (전략 패턴이 사용된 것이다.) 대부분의 HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태나 응답 메세지 등을 설정한다. 그래서 WAS 입장에서는 해당 요청이 정상적인 응답인 것으로 인식되며, 위에서 설명한 복잡한 WAS의 에러 전달이 진행되지 않는다.

예외가 던져지면 디스패처 서블릿까지 전달되는데, 적합한 예외 처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다. 그리고 적용 가능한 구현체를 찾아 예외 처리를 하는데, 우선순위대로 아래의 4가지 구현체들이 빈으로 등록되어 있다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리한다.

Spring은 아래와 같은 도구들로 ExceptionResolver를 동작시켜 에러를 처리할 수 있다.

  1. ResponseStatus
  2. ResponseStatusException
  3. ExceptionHandler
  4. ControllerAdvice, RestControllerAdvice
@RestControllerAdvice
public class ExceptionResponseAdvice {
    /**
     * BaseException 예외처리 핸들러
     * @param e BaseException
     * @return BaseResponse
     */
    @ExceptionHandler(BaseException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST) //응답코드: 400 에러로 일단 통일
    public BaseResponse handlerBaseException(BaseException e){
        return new BaseResponse(e.getStatus());
    }

    /**
     * Exception 예외처리 핸들러
     * @param e Exception
     * @return BaseResponse - 서버 내부에서 에러 발생
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public BaseResponse handlerException(Exception e){
        e.printStackTrace();
        return new BaseResponse(BaseExceptionStatus.SERVER_INTENER_ERROR);
    }

    /**
     * UnknownHostException 예외처리 핸들러
     * @param e Exception
     * @return BaseResponse - 링크가 잘못되었습니다.
     * @author 박현성
     */
    @ExceptionHandler({UnknownHostException.class, MalformedURLException.class})
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public BaseResponse unknownHostException(Exception e){
        return new BaseResponse(BaseExceptionStatus.DATA_NAME_INCORRECTION);
    }
			....생략
}

@ResponseStatus는 언제 사용하나?

어노테이션 이름에서 예측가능하듯이 @ResponseStatus는 에러 HTTP 상태를 변경하도록 도와주는 어노테이션이다.

회사 내규에 따라 다르겠지만 Exception이 발생하였을 경우에 Response Body에만 표시해줄지 아니면 Http status code에도 표시할지에 따라서 다를 수 있다. 만약 status code에도 어떤 에러인지 명시적으로 내어주고 싶을 경우에 @ResponseStatus를 사용할 수 있다.

@ExceptionHandler는 언제 사용하나?

@ExceptionHandler는 매우 유연하게 에러를 처리할 수 있는 방법을 제공하는 기능이다. @ExceptionHandler는 다음에 어노테이션을 추가함으로써 에러를 손쉽게 처리할 수 있다.

  • 컨트롤러의 메소드
  • @ControllerAdvice나 @RestControllerAdvice가 있는 클래스의 메소드

@ExceptionHandler는 Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있다. 만약 ExceptionHandler 어노테이션에 예외 클래스를 지정하지 않는다면, 파라미터에 설정된 에러 클래스를 처리하게 된다. 또한 @ResponseStatus와도 결합가능한데,  만약 ResponseEntity에서도 status를 지정하고 @ResponseStatus도 있다면 ResponseEntity가 우선순위를 갖는다.ExceptionHandler는 @ResponseStatus와 달리 에러 응답(payload)을 자유롭게 다룰 수 있다는 점에서 유연하다. 예를 들어 응답을 다음과 같이 정의해서 내려준다면 좋을 것이다.

  • code: 어떠한 종류의 에러가 발생하는지에 대한 에러 코드
  • message: 왜 에러가 발생했는지에 대한 설명
  • erros: 어느 값이 잘못되어 @Valid에 의한 검증이 실패한 것인지를 위한 에러 목록

Spring은 예외가 발생하면 가장 구체적인 예외 핸들러를 먼저 찾고, 없으면 부모 예외의 핸들러를 찾는다. 예를 들어 NullPointerException이 발생했다면, 위에서는 NullPointerException 처리기가 없으므로 Exception에 대한 처리기가 찾아진다.

@ControllerAdvice와 @RestControllerAdvice

Spring은 전역적으로 @ExceptionHandler를 적용할 수 있는 @ControllerAdvice와 @RestControllerAdvice 어노테이션을 각각 Spring3.2, Spring4.3부터 제공하고 있다. 두 개의 차이는 @Controller와 RestController와 같이 @ResponseBody가 붙어 있어 응답을 Json으로 내려준다는 점에서 다르다. ControllerAdvice 어노테이션에는 @Component 어노테이션이 있어서 ControllerAdvice가 선언된 클래스는 스프링 빈으로 등록된다. 그러므로 우리는 다음과 같이 전역적으로 에러를 핸들링하는 클래스를 만들어 어노테이션을 붙여줌으로써 에러 처리를 위임할 수 있다.

예외 복구 범위를 정해보자

메소드 영역 : 메소드 영역은 종속된 복구 기능으로 단순히 try, catch 사용 하면 된다.

클래스 영역 : 클래스 내 공통 예외 복구는 @ExceptionHandler 사용할 수 있다.

전역 영역 : 여러 클래스의 공통 예외 복구는 @ControllerAdvice 사용할 수 있다.

ControllerAdvice는 전역적으로 적용되는데, 만약 특정 클래스에만 제한적으로 적용하고 싶다면 @RestControllerAdvice의 basePackages 등을 설정함으로써 제한할 수 있다.

@ResponseStatus의 코드를 확인해보려면 HttpStatus 안으로 들어가보면 코드를 확인할 수 있다.

Scrap에서는 @ResponseStatus을 400으로 통일을 했습니다.

@Getter
public enum BaseExceptionStatus {

    TEST_ERROR(40401, "실패했어요"),
    SERVER_INTENER_ERROR(2002, "서버 내부적인 에러"),

    // 유저 관련은 3000번 에러
    DUPULICATE_USERNAME(3001, "아이디가 중복됩니다"),
    FAIL_ENCRYPT_PASSWORD(3002, "비밀번호 암호화에 실패했습니다"),
    LOGIN_USER_NOT_EXIST(3003, "해당하는 아이디 또는 비밀번호가 없습니다"),
    NOT_LOGIN_USER(3004,"로그인 하지 않은 유저입니다"),
    USER_NOT_EXIST(3005,"해당하는 유저가 존재하지 않습니다"),

    CATEGORY_NAME_NULL(4444, "카테고리 이름을 입력해주세요"),
    CATEGORY_NAME_LENGTH(4444, "카테고리 이름이 2~60 글자 사이"),
    DATA_NAME_INCORRECTION(44444, "링크가 잘못되었습니다."),
    MYPAGE_USER_NOT_FOUND(44444, "해당 사용자를 찾을 수 없습니다."),

    CATEGORY_NOT_EXIST(4444, "해당 카테고리자 존재하지 않습니다"),
    LINK_NOT_EXIST(4444, "해당 자료가 존재하지 않습니다"),
    LINK_AND_USER_NOT_CORRECT(4444, "해당 유저가 만든 자료가 아닙니다"),
    CATEGORY_AND_USER_NOT_CORRECT(4444, "해당 유저가 만든 카테고리가 아닙니다"),

	... 생략 ...
    
    private final int code;
    private final String message;

    private BaseExceptionStatus(int code, String msg){
        this.code = code;
        this.message = msg;
    }
}

BaseExceptionStatus Enum class

우리가 클라이언트에게 보내줄 에러 코드를 정의해야 한다. 기본적으로 에러 이름과 HTTP 상태 및 메세지를 가지고 있는 에러 코드 클래스를 만들었다. 발생할 수 있는 에러 코드를 위와 같이 정의했다.

Enum class란?

  • 클래스처럼 보이게 하는 상수
  • 서로 관련있는 상수들끼리 모아 상수들을 정의하는것
  • enum 클래스 형을 기반으로 한 클래스형 선언

Enum class 특징

  1. 열거형으로 선언된 순서에 따라 0부터 index 값을 가진다.(순차적으로 증가)
  2. enum 열거형으로 지정된 상수들은 모두 대문자로 선언한다.
  3. 열거형 변수들을 선언한 후 마지막에 세미콜론(;)을 찍지 않는다.
  4. 상수와 특정 값을 연결시킬경우 마지막에 세미콜론(;)을 붙여줘야한다.

상수와 특정값을 연결시켜놓은건데 특정값을 연결시키려면 해당 값들을 리턴할 수 있는 함수가 선언되어있어야한다.

@Getter
public class BaseException extends RuntimeException{

    private final BaseExceptionStatus status;

    public BaseException(BaseExceptionStatus status){
        super(status.getMessage());
        this.status = status;
    }
}

우리가 발생한 예외를 처리해줄 예외 클래스(Exception Class)를 추가해주어야 한다. 우리는 언체크 예외(런타임 예외)를 상속받는 예외 클래스를 다음과 같이 추가해줄 수 있다.

여기서 체크 예외가 아닌 언체크 예외를 상속받도록 한 이유가 있다. 왜냐하면 일반적인 비지니스 로직들은 따로 catch해서 처리할 것이 없므로 만약 체크 예외로 한다면 불필요하게 throws가 전파될 것이기 때문이다.

또한 Spring은 내부적으로 발생한 예외를 확인하여 언체크 예외이거나 에러라면 자동으로 롤백시키도록 처리한다. Spring에서 체크 예외만 롤백을 안하는 이유는 체크 예외는 처리가 강제되기 때문에 개발자가 무언가를 처리할 것이라는 기대 때문이다.

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"code", "message", "result"})
public class BaseResponse<T> {

    private int code;
    private String message;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T result;

    // 기본 생성자 막아둠
    private BaseResponse(){}

    public BaseResponse(T result){
        this.code = 1000;
        this.message = "요청에 성공했습니다";
        this.result = result;
    }

    public BaseResponse(String msg, T result){
        this.code = 1000;
        this.message = msg;
        this.result = result;
    }

    public BaseResponse(String msg){
        this.code = 1000;
        this.message = msg;
    }

    public BaseResponse(int code, String msg){
        this.code = code;
        this.message = msg;
    }

    public BaseResponse(BaseExceptionStatus e){
        this.code = e.getCode();
        this.message = e.getMessage();
    }
}

클라이언트로 정해진 포맷의 에러를 던져주도록 하기 위해 위와 같이 에러 응답 클래스를 추가해주었다. 위의 클래스는 다양한 응답에 처리를 해주기 위해 오버로딩을 사용하였다. 또한 만약 errors가 없다면 응답으로 내려가지 않도록 @JsonInclude 어노테이션을 추가하였다.

참조

https://www.nextree.co.kr/p3239/

https://mine-it-record.tistory.com/204

https://mangkyu.tistory.com/204

https://mangkyu.tistory.com/205

728x90
728x90

Labal 설정

프로젝트 레파지토리에서 Pull requests를 누르면 위와 같은 화면이 나온다. Labels를 클릭하면 아래와 같은 화면이 나온다.

이 화면은 New label을 눌렀을 때 나오는 화면입니다. Label name과 Description, Color를 작성과 선택해서 Create label를 눌러주면 된다.

이 화면은 실제로 Label을 어떻게 사용하고 있는지 보여주는 화면입니다.

상단의 Label을 클릭하면 원하는 Label의 Pull request만 볼 수 있다.

https://github.com/woowacourse-teams/2022-gong-seek/labels

 

GitHub - woowacourse-teams/2022-gong-seek: 공식(공유해줘 너의 지식) : 우테코 크루들을 위한 질문 게시판

공식(공유해줘 너의 지식) : 우테코 크루들을 위한 질문 게시판. Contribute to woowacourse-teams/2022-gong-seek development by creating an account on GitHub.

github.com

Pull Request 템플릿 만들기

프로젝트 상단에 .github 폴더를 만들고 PULL_REQUEST_TEMPLATE.md 파일의 이름으로 만든다.

Edit new file을 누르고 템플릿으로 사용할 틀을 만들어 준다.

## 요약

<br><br>

## 작업 내용

<br><br>

## 참고 사항

<br><br>

## 관련 이슈

- Close #이슈번호

<br><br>

Issue template 만들기

ISSUE_TEMPLATE 폴더를 만든다. github에서 폴더 만드는 법은 file 이름 넣는 칸에 폴더 이름을 넣고 / 를 입력하면 폴더가 생긴다. 그 전으로 돌아가려면 .. 을 입력하면 된다.

위의 화면은 Issue template 작성하는 코드이다.

---
name: Bug template
about: 버그 발생 시 사용하는 템플릿입니다
title: "버그 리포트"
labels: "\\U0001F41B BUG"
assignees:

---

# 버그 리포트

## 어떤 버그인가요?

> 버그에 대해 알려주세요

<br><br>

## 어떤 상황에서 겪으셨나요?

> 버그를 겪으신 상황을 알려주세요

<br><br>

## 참고할만한 자료가 있을까요?

> 참고자료가 있다면 첨부해주세요

<br><br>

Pull Request 템플릿 작성되는지 테스트

PR 작성하는 식으로 Compare & request버튼을 누른다.

Pull Request 템플릿으로 작성했던 코드가 잘 불러와지는 것을 확인할 수 있다.

ISSUE 템플릿 확인

New Issue를 누른다.

Pull Request와는 다르게 템플릿 선택하는 화면이 나온다. Get started를 누르면 템플릿이 잘 불러와진다.

참고: https://github.com/woowacourse-teams/2022-gong-seek/labels

 

GitHub - woowacourse-teams/2022-gong-seek: 공식(공유해줘 너의 지식) : 우테코 크루들을 위한 질문 게시판

공식(공유해줘 너의 지식) : 우테코 크루들을 위한 질문 게시판. Contribute to woowacourse-teams/2022-gong-seek development by creating an account on GitHub.

github.com

 

728x90
728x90

Text 위젯

Text위젯의 속성 중에 style이라고 있다. style에는 넣을 수 있는 데이터 타입은 무엇인가? Type: TextStyle이다.

TextStyle에 대해 알아보자.

글자를 우리가 원하는대로 색을 넣거나, 크기를 조정하거나 할 수 있다.

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('비밀 게시판'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: ListView(
            children: [
              ListTile(
                title: Text('공지사항'),
                subtitle: Text(
                  '반갑습니다. 여러분',
                  style: TextStyle(
                    color: Colors.green,
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                leading: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Icon(Icons.book),
                ),
                trailing: Icon(Icons.navigate_next),
              ),
              ....,
							....,
            ],
          ),
        ),
      ),
    );
  }

AppBar

지금까지 사용했던 속성은 leading, title, centerTitle, backgroundColor이고, 오늘 추가로 배워볼 속성은 foregroundColor, titleTextStyle, actions이다.

  1. leading: 앱바에 제일 앞에오는 위젯을 설정할 수 있음
  2. title : 앱의 타이틀(주로 텍스트 혹은 이미지)을 설정할 수 있음
  3. centerTitle : 타이틀을 가운데로 설정할 것인지
  4. backgroundColor : 앱바의 배경색을 결정
  5. foregroundColor : 앱바의 기본 표현 색을 결정
  6. titleTextStyle : title이 Text위젯을 썼다면, 적용할 텍스트 스타일 (foregroundColor 보다 힘 쌤 )
  7. actions : 앱 바 제일 뒤에 오는 위젯을 설정할 수 있음. 주의사항 AppBar는 trailing이 아니라 actions로 배열이 들어간다.

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('연락처'),
          centerTitle: false,
          backgroundColor: Colors.white,
          foregroundColor: Colors.black,
          elevation: 0,
          actions: [
            Icon(Icons.settings),
            Icon(Icons.settings),
          ],
        ),
      ),
    );
  }

CircleAvatar 위젯

보통 아바타를 그릴 때 사용. 이미지와 함께 사용하여 동그란 이미지를 제공한다. 프로필 사진 혹은 채팅방 명, 로고 등의 이미지를 보여주기에 좋다.

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('연락처'),
          centerTitle: false,
          backgroundColor: Colors.white,
          foregroundColor: Colors.black,
          elevation: 0,
        ),
        body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: ListView(
            children: [
              ListTile(
                title: Text('빈 유저'),
                leading: CircleAvatar(),
              ),
            ],
          ),
        ),
      ),
    );
  }

child안에 들어갈 위젯을 제공한다, 텍스트 위젯을 이용해서 이미지가 없을 경우 연락처의 경우 한 글자만 넣는 경우가 있다.

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('연락처'),
          centerTitle: false,
          backgroundColor: Colors.white,
          foregroundColor: Colors.black,
          elevation: 0,
          actions: [
            Icon(Icons.settings),
            Icon(Icons.settings),
          ],
        ),
        body: ListTile(
          leading: CircleAvatar(
            child: Text('빈'),
          ),
          title: Text('빈 유저'),
        ),
      ),
    );
  }

child 에 아이콘 위젯을 넣을 경우

프로필 사진 등을 구현하려 할 때, child를 넣어 이미지 위젯을 부르는 것보다는 backgroundImage를 사용하는 것이 추천된다.

backgroundImage: Image.network(’<https://picsum.photos/200’>),

Image.network()로 구현하면 오류가 나는 것을 확인할 수 있다.

그 이유는 backgroundImage 속성은 위젯을 전달받지 않는다. ImageProvider의 데이터형태만 전달받는다.

ImageProvider인 데이터 형태의 종류는 크게 4가지이다.

  • NetworkImage
  • AssetImage
  • FileImage
  • MemoryImage

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('연락처'),
          centerTitle: false,
          backgroundColor: Colors.white,
          foregroundColor: Colors.black,
          elevation: 0,
          actions: [
            Icon(Icons.settings),
            Icon(Icons.settings),
          ],
        ),
        body: ListTile(
          leading: CircleAvatar(
            backgroundImage: NetworkImage('<https://picsum.photos/200>'),
          ),
          title: Text('환경 설정'),
        ),
      ),
    );
  }

CircleAvatar 위젯

adius(반지름)의 크기를 키워 아바타의 사이즈를 정해줄 수 있다.

radius라는 속성에 입력된 int(정수)의 수를 조정하여 크기 조절할 수 있다.

body: CircleAvatar(
          radius: 100,
          backgroundImage: NetworkImage('<https://picsum.photos/200>'),
        ),

Scaffold의 능력

Scaffold의 속성 중, appBar와 body에 위젯을 각각 넣었었다.

바로 흔히 말하는 [바텀네비게이션바, BottomNavigationBar] 그리고 [FAB, FloatfingActionBar]

BottomNavigationBar 위젯

어플은 화면을 나누어 특정한 목적을 달성하기 시키기 위한 스크린들이 존재한다.

카카오톡 → 친구창 / 채팅방 리스트 / 쇼핑 / 내 설정 인스타그램 → 피드 / 검색 / 릴스 / 쇼핑 / 내 피드

Scaffold가 이러한 기능을 사용할 수 있도록 속성을 제공한다.

BottomNavigationBar 위젯은 BottomNavigationBarItem이 하나만 있으면 오류가 발생한다.

bottomNavigationBar: BottomNavigationBar(
          items: [
            BottomNavigationBarItem(icon: Icon(Icons.call), label: '연락처'),
            BottomNavigationBarItem(icon: Icon(Icons.block), label: '차단'),
            BottomNavigationBarItem(icon: Icon(Icons.settings), label: '설정'),
          ],
        ),

BottomNavigationBar는 items라는 속성을 필수(required)로 받습니다. 그 안은 무조건 BottomNavigationBarItem만 올 수 있다.

BottomNavigationBar는 조건이 있습니다.

  1. 여러 페이지를 위해 제공되는 거라서 버튼(BottomNavigationBarItem)이 두 개 이상이어야 함
  2. 각 BottomNavigationBarItem에 label속성을 넣어줍니다.

FloatingActionButton 위젯

floatingActionButton: FloatingActionButton(
          onPressed: () {},
        ),
        bottomNavigationBar: BottomNavigationBar(
          items: [
            BottomNavigationBarItem(icon: Icon(Icons.call), label: '연락처'),
            BottomNavigationBarItem(icon: Icon(Icons.block), label: '차단'),
            BottomNavigationBarItem(icon: Icon(Icons.settings), label: '설정'),
          ],
        ),
728x90

'개발 > Flutter' 카테고리의 다른 글

Flutter 여섯 번째 강의 내용  (0) 2022.11.01
Flutter 다섯 번째 강의 내용  (0) 2022.10.27
Flutter 세 번째 강의 내용  (0) 2022.10.21
Flutter 두 번째 강의 내용  (0) 2022.10.20
Flutter 첫 번째 강의 내용  (0) 2022.10.18
728x90

개요

기존에 Jasypt 패키지를 이용해 YML 설정 파일 암호화를 진행하였지만 application.yml 파일 내 jasypt.encryptor.password가 노출되어있어 암호화는 해주었지만 암호화를 풀 수 있는 열쇠를 노출해 이와 같은 문제를 해결하는 방법으로 Github Repository secrets을 이용해 해결하였습니다.

떠오른 방법

  1. 암호화를 풀 수 있는 키를 우분투 서버 자체에 환경변수로 등록하여서 도커 컨테이너에 스프링 run 할 때 환경변수를 전달한다.
  2. Github Repository secrets를 이용한다.
  3. Spring Vault를 이용한다.

Spring Vault란?

Vault는 HashiCorp에 의해서 개발된 크로스 플랫폼 패스워드 및 인증 관리 시스템이다. 공개되면 안 되는 비밀번호, API 키, 토큰 등을 저장하고 관리한다.

 

선택한 방법

우선 저는 제일 간단한 방법으로 진행하기 위해 2번을 선택했습니다.

시간이 되면 1번 방법과 3번 방법으로 진행해 보겠습니다.

 

해결 방법

1. GitHub 해당 Repository에 들어가서 Settings에서 왼쪽 하단에 있는 Secrets의 Actions에 접속한다.

2. Secrets의 Actions에서 오른쪽 상단에 있는 New repository secret을 선택한다.

3. secret의 Name을 입력하고 Secret에는 암호화를 진행하고 싶은 내용을 입력하고 Add secret을 누르면 생성된다.

4. 현재 제가 만든 secret 목록입니다.

5. .github/workflows/gradle.yml 파일에 아래의 코드를 추가한다.

Repository secrets을 사용하면 GitHub Actions의 gradle.yml에만 환경변수가 적용되어 스프링에서 사용할 수 있게 변수를 넘겨주어야 한다.

- name: Set Yaml
  uses: microsoft/variable-substitution@v1
  with:
    files: ./src/main/resources/application-prod.yml 
  env:
    spring.datasource.url: ${{ secrets.DB_URL }} 
    spring.datasource.username: ${{ secrets.DB_USERNAME }} 
    spring.datasource.password: ${{ secrets.DB_PASSWORD }}

gradle.yml 전체 코드

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle>
name: Java CI with Gradle

on:
  push:
    branches: [ "main" ]
#   pull_request:
#       branches: [ "main" ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
    - name: Set Yaml
      uses: microsoft/variable-substitution@v1
      with:
        files: ./src/main/resources/application-prod.yml 
      env:
        spring.datasource.url: ${{ secrets.DB_URL }} 
        spring.datasource.username: ${{ secrets.DB_USERNAME }} 
        spring.datasource.password: ${{ secrets.DB_PASSWORD }} 
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build with Gradle
      run: ./gradlew build
    - name: Docker build
      run: |
       docker login -u ${{ secrets.USERNAME }} -p ${{ secrets.PASSWORD }}
       docker build -t spring-cicd2 .
       docker tag spring-cicd2 lusida0131/spring-cicd2:latest
       docker push lusida0131/spring-cicd2:latest
  deploy:
    needs: build  # build 후에 실행되도록 정의
    name: Deploy
    runs-on: [ self-hosted, label-go ] # AWS ./configure에서 사용할 label명
    steps:
      # 3000 -> 80 포트로 수행하도록 지정
      - name: Docker run
        run: |
          docker login -u ${{ secrets.USERNAME }} -p ${{ secrets.PASSWORD }}
          docker stop spring-cicd2 && docker rm spring-cicd2 && docker rmi lusida0131/spring-cicd2:latest
          docker run -d -p 8081:8081 --name spring-cicd2 --restart always lusida0131/spring-cicd2:latest

변수는 ${{ secrets.적용한이름 }}으로 불러올 수 있다. 위의 코드를 보면 ./src/main/resources/application-prod.yml의 파일에 spring.datasource.url를 ${{ secrets.DB_URL }} 변수로 덮어 씌운다. 실제 application-prod.yml의 값은 임의의 값을 주어도 상관없다.

datasource:
    url: ${url}
    username: ${username}
    password: ${password}
728x90
728x90
  1. numOfLink라는 해당 카테고리 안에 있는 자료의 count 개수를 세는 게 있는데 카테고리 개수만큼 select count 쿼리문이 날아간다. select count가 날아가는데 운영하는데 부하가 있지 않을까?
  2. 카테고리 순서 변경을 할 때 하나를 바꿀 때마다 업데이트 쿼리문이 날아가는데 사용자가 카테고리 순서를 한 번에 많이 바꿀 경우 select 쿼리와 update 쿼리가 계속 날아가고 프런트와 계속 api 통신을 해야 하는데 부하가 심하게 걸리지 않을까? 실무에서는 어떻게 통신하고 수정하는지 현재 저희는 변경할 때마다 api 통신을 한다.

 

728x90
728x90

column과 Row는 기본 값

column과 Row는 기본값이 있어서 crossAxisAlignment: CrossAxisAlignment.start를 붙여줘야 한다.

body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('오늘 수업도!'),
              Text('오늘 수업도!'),
              Container(
                height: 40,
              ),
              Text('이력'),
              Text('멍때리기 대회 1등'),
              Text('밥 천천히 먹기 1등'),
            ],
          ),
        ),

padding 위젯

Padding 위젯을 넣어주면 부모와 자식 사이의 간격이 벌어집니다.

Padding 위젯의 필수 속성인 padding에다가 EdgeInsets.all(원하는 간격) 넣기

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('내 정보 앱'),
          centerTitle: false,
        ),
        body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('이름: 박현성'),
              Text('나이: 26'),
              Text('성별: 남자'),
              Container(
                height: 40,
              ),
              Text('이력'),
              Text('멍때리기 대회 1등'),
              Text('밥 천천히 먹기 대회 1등'),
              Text('치킨 많이 먹기 대회 1등'),
            ],
          ),
        ),
      ),
    );
  }

EdgeInsets 이란

  • 마진, 패딩 등의 여백을 줄 때 사용하는 데이터 타입

EdgeInsets.all(4) → 상하좌우 전부 4만큼만 띄워줘.

EdgeInsets.all(8) → 상하좌우 전부 8만큼만 띄워줘.

EdgeInsets.only(left: 8) → 왼쪽에서 8만큼만 띄워줘.

EdgeInsets.LTRB(20,15,10,5) → 왼 20, 위 15, 오 10, 아래 5 만큼 띄워줘.

margin과 padding 차이

padding은 Container의 안쪽 영역의 여백

margin은 Container의 바깥 영역의 여백이다.

 

Container 위젯

자식 위젯을 하나로 묶어서 관리하기 좋은 위젯, 컨테이너로 감싼다.

주로 둥글게 깎거나, 사이즈를 넣어주거나, 색상을 넣어주거나, 간격을 띄워주는 등 많은 기능이 있음.

body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('이름: 박현성'),
                Text('나이: 26'),
                Text('성별: 남자'),
                Container(
                  height: 40,
                ),
                Text('이력'),
                Text('멍때리기 대회 1등'),
                Text('밥 천천히 먹기 대회 1등'),
                Text('치킨 많이 먹기 대회 1등'),
              ],
            ),
          ),
        ),

Container에서 함께 쓰는 속성들 중 오늘 살펴볼 자주 쓰이는 속성

color : 컨테이너의 색상 width : 컨테이너의 넓이 (픽셀) height : 컨테이너의 높이 (픽셀) child : 자식 위젯

body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
            color: Colors.red,
            width: 100,
            height: 150,
          ),
        ),

Icon 위젯

앱에 들어가는 아이콘을 가져올 수 있는 아이콘 위젯, Material Icon들을 기본적으로 사용할 수 있다.

https://fonts.google.com/icons?selected=Material+Icons

Icon 위젯 안에, Icons. 입력하면, 자동완성 기능을 통해 아이콘을 삽입할 수 있음.

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('내 정보 앱'),
          centerTitle: false,
        ),
        body: Row(
          children: [
            Icon(
              Icons.person,
            ),
            Icon(
              Icons.phone,
            ),
            Icon(
              Icons.settings,
            ),
          ],
        ),
      ),
    );
  }

ListTile 위젯

목록을 통해서 보여주고 싶은 정보를 일자로 나열할 때 일관성을 유지하기 위함

title에 Text위젯을 넣어 이름을 넣고, subtitle에 Text위젯을 넣어 번호를 입력한 뒤에, leading의 속성에 Icon 혹은 Avatar를 넣을 수 있음.

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('내 정보 앱'),
          centerTitle: false,
        ),
        body: ListTile(
          title: Text('박현성'),
          subtitle: Text('010-0000-0000'),
          leading: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Icon(Icons.person),
          ),
          trailing: Icon(Icons.phone),
        ),
      ),
    );
  }

ListTile의 가장 기본은, title속성 (한 줄)로 만들어져 있기 때문에 subtitle속성이 들어오며 두 줄이 되어 기존의 아이콘에 추가 간격이 필요하다. 우린 이걸 Padding 위젯을 넣어 해결해줄 수 있다.

leading은 가장 앞에 둘 때, trailing은 가장 뒤에 둘 때, trailing의 속성 값에 Icon위젯을 넣을 수 있다.

import 'package:flutter/material.dart';
// ignore_for_file: prefer_const_constructors

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('연락처 앱'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Container(
            width: 350,
            height: 200,
            color: Colors.grey,
            child: Center(
              child: Text(
                '안녕하세요',
              ),
            ),
          ),
        ),
      ),
    );
  }
}

화면에 너무 많이 입력하면, 화면상에 표현할 수 있는 높이를 벗어나게 됨.

SingleChildScrollView 위젯

자식에게 스크롤이 가능하도록 스크롤 기능을 제공, Column에게 사용해주면, 스크롤을 사용하여 오버플로우가 일어나지 않도록 해준다.

주로 Column, Row가 함께 많이 쓰이며, 사이즈가 클 것 같거나 핸들링하기 어려운 사이즈의 위젯을 다룰 때 좋다.

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('내 정보 앱'),
          centerTitle: false,
        ),
        body: SingleChildScrollView(
          child: Column(
            // ignore: prefer_const_literals_to_create_immutables
            children: [
              ListTile(
                title: Text('박현성'),
                subtitle: Text('010-0000-0000'),
                leading: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Icon(Icons.person),
                ),
                trailing: Icon(Icons.phone),
              ),
							....
							....
            ],
          ),
        ),
      ),
    );
  }

  • Column 위젯은, 본인이 차지할 수 있는 무한대의 수직 영역을 사용한다. 하지만 그렇기 때문에, 화면이 넘치면 어떻게 할 줄 모르는데, SingleChildScrollView 위젯 = 스크롤 능력을 주기
  • 근데 이 둘의 장점을 하나로 합쳐서 사용할 수 있는 것이 ListView위젯, 기본 행동(behavior)이 스크롤이 들어가 있다.
@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('내 정보 앱'),
          centerTitle: false,
        ),
        body: ListView(
          // ignore: prefer_const_literals_to_create_immutables
          children: [
            ListTile(
              title: Text('박현성'),
              subtitle: Text('010-0000-0000'),
              leading: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Icon(Icons.person),
              ),
              trailing: Icon(Icons.phone),
            ),
            ....,
						....
          ],
        ),
      ),
    );
  }

 

728x90

'개발 > Flutter' 카테고리의 다른 글

Flutter 다섯 번째 강의 내용  (0) 2022.10.27
Flutter 네 번째 강의 내용  (0) 2022.10.24
Flutter 두 번째 강의 내용  (0) 2022.10.20
Flutter 첫 번째 강의 내용  (0) 2022.10.18
Flutter 환경 구성 및 설치  (2) 2022.10.14
728x90

플러터 첫 프로젝트 만들기

Visual Studio Code에서 커멘드 창을 연다. 단축키로는 command + shift + p이고 직접 실행하는 방법은 View에서 Command Palette이다.

 

Flutter: New Project를 누르면 아래와 같은 사진이 나오는데 Application을 클릭한다.

 

그러면 이제 폴더를 선택하는 창이 나온다. 원하는 폴더에 위치시키고 Select a folder to create the project in을 누르면 된다.

 

이제 원하는 프로젝트 이름을 입력해주면 된다.

 

Flutter 폴더 구조 확인하기

플러터를 프로젝트를 만들면 많은 양의 폴더와 파일들이 생성되는데 겁먹지 말고 사용하는 것만 주로 사용한다고 한다.

초급에서 볼 필요 없는 것들

.dart_tool, .idea, flutter_scrap.iml, pubspec.lock, README.md 파일 등이 있다.

차츰차츰 알아서 습득 될 것들

android, ios, linux, macos, web, windows, test 등이 있다.

초급 이어도 필수로 알아야 할 것들

lib, pubspec.yaml 이 있다.

lib 폴더

흔히 Library 폴더라고 한다. 줄여서 lib이라고 표기한다.

우리가 작성하는 모든 코드가 lib 폴더에서 작성된다.

기본적으로 main.dart 파일이 들어가 있다.

pubspec.yaml

앱을 만들 때 필요한 패키지나 이미지, 폰트 등을 등록하는 설정 파일이다.

main.dart

앱의 시작점이라서, 이 파일이 없으면 오류 나고 실행이 안됨

플러터 프로젝트 생성 시에 기본적으로 생성되는 파일

실행할 디바이스 선택

visual studio Code 하단 바에 macOS를 클릭한다.

 

클릭하면 상단에 어떤 디바이스를 선택할 것인지 창이 나온다.

 

이제 프로젝트를 실행시키면 된다.

프로그램의 시작점

  1. lib/main.dart 실행
  2. main.dart 안의 main을 찾기
  3. main 안에 runApp을 찾기
import 'package:flutter/material.dart';
// ignore_for_file: prefer_const_constructors

void main() => runApp(MyApp());
  1. material.dart라는 패키지를 사용하겠다.
  2. main을 실행하면, runApp을 실행시킬 것이고, runApp은 MyApp을 실행시킨다.

새로운 줄에서 stl만 입력하면 자동으로 Flutter stateless Widget이라는 영역이 생긴다.

Flutter stateless Widget을 선택한다. 자동완성 기능이라고 생각하면 된다. 정식 명칭으로는 snippet이다

 

return Container() 부분을 MaterialApp으로 변경한다.

 

글자가 왼쪽 상단에 잘려서 보이는 것을 확인할 수 있다.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Text("안녕하세요 키키"),
      ),
    );
  }
}

MaterialApp 위젯

  • 구글은, 일관성 있는 모바일 디자인을 추구했는데, 그 결과 구글의 머티리얼 디자인이 탄생했다. https://material.io/design/ 에서 확인할 수 있는 디자인 가이드가 있다.
  • 그 일관성 있는 디자인을 사용할 수 있도록 초기 틀을 잡아주는 역할
  • 즉, 그림 그리기 전 도화지를 준비해 놓는 것과 같다고 생각하면 좋다.
  • 가장 최상위의 위젯 (최상위의 부모)라서 단 하나의 MaterialApp 안에 여러 위젯들이 살 수 있음

Scaffold 위젯

  • 그래서 그 머티리얼 위젯을 사용할 수 있도록 실제 건축 준비를 해주는 친구
  • Scaffold 위젯에 AppBar 위젯을 넣을 수도 있고, 공중에 떠있는 버튼도 넣어볼 수 있다

MaterialApp + Scaffold는 짝꿍이고, 99%의 모든 앱이 함께 쓰고 있다. 같이 쓰는 위젯 정도로 외우면 좋다

SafeArea 위젯 디바이스마다, 카메라 노치나 아이폰 M자 탈모 등의 요소 화면 시작점이 각각 다르니 시작점을 통일하기 위해 사용

import 'package:flutter/material.dart';
// ignore_for_file: prefer_const_constructors

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: Text("안녕하세요 키키"),
        ),
      ),
    );
  }
}

Text 위젯

글자나 숫자 등을 화면상에 보여줄 때 사용하는 위젯, 바로 반드시 String형태의 데이터를 넘겨주자 이때 Strin형태의 데이터란, 따옴표로 감싼 문자열이다.

Center 위젯

자식 위젯을 가운데로 위치할 수 있도록 능력을 주는 부모

import 'package:flutter/material.dart';
// ignore_for_file: prefer_const_constructors

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: Center(
            child: Text("안녕하세요 키키"),
          ),
        ),
      ),
    );
  }
}

AppBar 위젯

어플에서 앱바를 그려줄 수 있는 위젯, 반드시 부모 위젯이 Scaffold여야 한다. 왜냐하면, Scaffold위젯 기능 중에 AppBar를 쓸 수 있는 기능이 있기 때문임.

여기서 알 수 있는 Scaffold 기능

  1. AppBar에 위젯을 넣을 수 있다.
  2. Body에도 위젯을 넣을 수 있다.
import 'package:flutter/material.dart';
// ignore_for_file: prefer_const_constructors

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("내 정보 앱"),
        ),
        body: SafeArea(
          child: Center(
            child: Text("안녕하세요 키키"),
          ),
        ),
      ),
    );
  }
}

 

그리고 앱바에는 다양한 기능을 넣어줄 수 있는데, 테스트해보기 좋은 기능들은 다음과 같다 ( backgroundColor, centerTitle )

 

bool, 불리언이라고 불리며 값은 진실(true) 혹은 거짓(false)밖에 없다. 여기에서는 파란색으로 색깔이 구분된다.

centerTitle: false,

Column 위젯

텍스트로 개행 문자(\n)를 넣어서 해줄 수 있지만, 위젯은 다음 줄에 못 넣는다. 위젯들을 수직방향으로 나열하고 싶을 경우 사용한다, child가 아닌, child의 복수형인 children을 사용한다 (다수의 위젯을 갖기에)

  • 기본값으로, 수직적으로 빈 영역이 있으면 다 차지해버리는 설계 방식
import 'package:flutter/material.dart';
// ignore_for_file: prefer_const_constructors

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.purple,
          centerTitle: false,
          title: Text("내 정보 앱"),
        ),
        body: SafeArea(
          child: Column(
            children: [
              Text("안녕하세요 키키"),
              Text("안녕하세요 키키"),
              Text("안녕하세요 키키"),
            ],
          ),
        ),
      ),
    );
  }
}

 

Image.network 위젯

모든 이미지를 불러와서 사용할 수 있다. 하지만, 이미지를 어디서 가져오냐가 중요하다.

  1. 이미 업로드돼있는 네트워크를 통해 (인터넷, 웹)에서 가져올 경우 = Image.network()
// ignore_for_file: prefer_const_constructors

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.purple,
          centerTitle: false,
          title: Text('내 정보 앱'),
        ),
        body: SafeArea(
            child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Image.network(
                '<https://search.pstatic.net/common/?src=http%3A%2F%2Fblogfiles.naver.net%2FMjAyMjEwMDNfNTMg%2FMDAxNjY0ODAyNTk1OTMx.y-L9TXW9UQd4RjloZNI8_O8rKTZyfvbp2fLF2eZzNPog.zPQxjOaiSPPEkuDUFUWxJGzahFjzxybxVQz1SFy1pYwg.JPEG.kmcmodooall%2F1664802595768.jpg&type=sc960_832>'),
            Text('이름: 박현성'),
            Text('나이: 26'),
            Text('성별: 남자'),
          ],
        )),
      ),
    );
  }
}

 

현재까지 배운 데이터 타입은 (String, int, bool, widget) 총 4가지, 근데 위젯마다 사용하는 방법이 다르다.

예를 들어서

  1. Center 위젯의 child는 Widget을 넣을 수 있다.
  2. Text 위젯의 값은 String만 넣을 수 있다.
  3. Scaffold 위젯의 키 appBar의 값은 Widget만 넣을 수 있다.
  4. Scaffold 위젯의 키 body의 값은 Widget만 넣을 수 있다.
  5. Image.network 위젯의 값은 String만 넣을 수 있다.

Tips.

// ignore_for_file: prefer_const_constructors

위의 코드를 주석 달린 채로 작성하면 파란색 물결 밑줄 표시가 사라진다.

 

부모 자식 관계 자동완성

command +  . 이다.

 

728x90

'개발 > Flutter' 카테고리의 다른 글

Flutter 다섯 번째 강의 내용  (0) 2022.10.27
Flutter 네 번째 강의 내용  (0) 2022.10.24
Flutter 세 번째 강의 내용  (0) 2022.10.21
Flutter 첫 번째 강의 내용  (0) 2022.10.18
Flutter 환경 구성 및 설치  (2) 2022.10.14

+ Recent posts