이미지 출처: https://stackoverflow.com/questions/11541338/should-i-use-arraylist-or-list/11541387#11541387

Set, List Interface 는 Collection 을 extends 하고 있다 (구조 동일)

Map Interface 는 별도로 정의돼있다.

 

[List]

순서가 있는 데이터의 집합, 데이터 중복을 허용

ArrayList, Vector, LinkedList 차이에 대한 설명

 

[Set]

순서가 없는 데이터의 집합, 데이터 중복을 허용하지 않음

HashSet, LinkedHashSet, TreeSet 차이에 대한 설명

 

[Map]

키와 값, 한 쌍으로 이루어지는 데이터의 집합으로 순서가 없음

값은 중복을 허용하나 키는 중복될 수 없음(키는 중볼될 경우 덮어 씀)

HashTable, HashMap, LinkedHashMap, TreeMap 차이에 대한 설명

 

[Queue]

First In First Out 구조를 따름

LinkedList, PriorityQueue 차이에 대한 설명

 

 

반응형

'back > java' 카테고리의 다른 글

[Java] Generic 제네릭  (0) 2019.12.28
[Java] Compile  (0) 2019.12.22
Comparator, Comparable + Arrays.sort() 그리고 인터페이스..  (0) 2019.09.02
깊은복사(Deep Copy)와 얕은복사(Shallow Copy)  (3) 2019.08.31
LocalHost IP 가져오기  (0) 2019.05.28

※ 들어가기에 앞서, Spring Framework, maven, jboss(wildfly) 환경에서 log4j설정하는 법은 이곳을 참고.

 

log4j.xml 파일을 아래와 같이 설정한다.

pom.xml 및 기타 설정은 되어있다 가정

 

[log4j.xml]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
 
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
        </Console>
    </Appenders>
    
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
 
</Configuration>
 
cs

 

위에 설정한 콘솔 로그를 출력하기 위해 아래와 같이 Sample.class 를 만들어준다.

[Sample.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.log;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class Sample {
    
    protected static final Logger logger = LoggerFactory.getLogger(Sample.class);
    protected static final Logger fileLogger = LoggerFactory.getLogger("fileLogger");
    
    public static void main(String[] args) {
        
        logger.info("Sample.class logger print with info level");
        
        fileLogger.info("fileLogger logger print with info level");
    }
 
}
 
cs

"fileLogger 를 name 으로 하는 logger 가 없는데?" 라는 생각이 들겠지만 일단 main 메소드를 실행시켜보자.

 

[Sample.class 실행결과]

INFO : com.log.Sample - Sample.class logger print with info level
INFO : fileLogger - fileLogger logger print with info level

Exception을 찍거나, 혹은 아무것도 찍지 않을 줄 알았던 fileLogger.info(~); 가 로그를 정상적으로 찍고 있다.

이를 이해하려면 log4j 의 위계구조 (hierarchy) 에 대한 이해가 필요하다.

 

[log4j 위계구조 (hierarchy)]

1. 모든 로거의 최상위 부모는 Root 로거이다.

2. class 에서 로그를 출력하는데 사용된 logger가 log4j.xml 에 존재하지 않는다면, 부모 로거를 찾는다.

: Sample.class 9번 라인의 fileLogger 는 "fileLogger" 라는 name을 갖는 로거를 log4j.xml 에서 찾았으나, 해당 이름의 로거가 존재하지 않으므로 부모인 Root 로거를 타게된 것. (마찬가지 이유로, Sample.class 에서 8번라인의 ~.getLogger(Sample.class) 부분을 ~.getLogger("") 과 같이 바꿔도 로거는 정상출력된다)

 

 

이번엔 log4j 를 다음과 같이 수정 후, 위의 Sample.class 메인메소드를 다시 실행해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
        </Console>
    </Appenders>
    
    <Loggers>
        <Logger name="com" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Logger name="com.log" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Logger name="com.log.Sample" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
    
</Configuration>
 
cs

 

[실행 결과]

INFO : com.log.Sample - Sample.class logger print with info level
INFO : com.log.Sample - Sample.class logger print with info level
INFO : com.log.Sample - Sample.class logger print with info level
INFO : com.log.Sample - Sample.class logger print with info level
INFO : fileLogger - fileLogger logger print with info level

com.log 패키지 경로에 있는 Sample.class (Sample.class 의 1번라인 참고)는 자신의 클래스명과 일치하는 name을 가진 Logger를 찾는다.

com.log.Sample 로거의 appender 로 로그가 찍히고, 

com.log 로거의 appender 로 로그가 찍히고,

com 로거의 appender 로 로그가 찍히고,

Root 로거의 appender 로 로그가 찍혀 총 4번의 로그가 찍혔다. (fileLogger 제외)

(부모 > Root > com > com.log > com.log.Sample > 자식)

 

이번엔 아래와 같이 com.log 로거에 additivity="false" 속성을 추가해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
        </Console>
    </Appenders>
    
    <Loggers>
        <Logger name="com" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Logger name="com.log" level="INFO" additivity="false">
            <AppenderRef ref="console" />
        </Logger>
        
        <Logger name="com.log.Sample" level="INFO" >
            <AppenderRef ref="console" />
        </Logger>
        
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
    
</Configuration>
cs

 

[실행결과]

INFO : com.log.Sample - Sample.class logger print with info level 
INFO : com.log.Sample - Sample.class logger print with info level 
INFO : fileLogger - fileLogger logger print with info level

com.log.Sample 로거의 appender 로 로그가 찍히고,

com.log 로거의 appender 로 로그가 찍히고 끝이난다.

additivity 값을 false 로 주면 부모 로거로의 전달을 막게된다. (default 값은 true)

 

 

[실제 응용]

1. 특정 레벨의 로그는 별도의 파일로 관리

콘솔로그는 INFO 레벨 이상(INFO < WARN < ERROR...)인 경우 출력,

ERROR(ERROR < FATAL) 레벨 이상의 로그는 별도의 파일에 작성하여 관리하고자 하는 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
            <ThresholdFilter level="INFO"/>
        </Console>
        
        <RollingFile name="logFile" fileName="/logs/api.log" filePattern="/logs/$${date:yyyyMM}/app_%d{yyyyMMdd}-%i.log.gz">
            <PatternLayout pattern="%-5p: %m%n" />
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" />
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <ThresholdFilter level="ERROR"/>    
        </RollingFile>    
    </Appenders>
    
    
    <Loggers>
        <Logger name="com" additivity="false">
            <AppenderRef ref="console" />
            <AppenderRef ref="logFile" />
        </Logger>
          
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
</Configuration>
 
cs

FileAppender 는 로그를 파일에 작성하기 위해,

RollingFileAppender 는 로그를 파일에 저장 + 롤아웃(백업) 하기 위해 사용한다.

이외에도 다양한 Appender 들이 지원되니 다른 appender 가 필요하다면 공식 document 를 참고하면된다.

 

TimeBasedTriggeringPolicy 는 시간에 의한 백업

SizeBasedTriggeringPolicy 는 로거 파일 용량에 의한 백업

filePattern 은 백업 파일형식

ThresholdFilter 는 appender 의 로그 레벨을 지정

 

위 설정은 아래와 같다.

com 밑의 모든 class 에서 출력하는 INFO 레벨 이상의 콘솔 로그를 %-5p: %c - %m%n 포맷으로 찍는다.

com 밑의 모든 class 에서 출력하는 ERROR 레벨 이상의 콘솔 로그를 %-5p: %m%n 포맷으로 /logs/api.log 파일에 기록한다.

용량이 100MB 초과할 경우, 혹은 하루가 지난 경우(매일) 로그 파일을 /logs/년월/app_년월일-백업시퀀스.log.gz 이름으로 압축한다.

 

 

※ logback에서의 설정은 아래와 같다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- doc : http://logback.qos.ch/documentation.html -->
<!-- xml example : https://mkyong.com/logging/logback-xml-example/ -->
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
      </encoder>
   </appender>
 
   <appender name="FILE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>/boot_jpa_log/log_stats.log</file>
      <encoder>
         <pattern>%m%n</pattern>
      </encoder>
      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
         <fileNamePattern>/boot_jpa_log/old/log_stats_%d{yyyyMMdd}.log</fileNamePattern>
      </rollingPolicy>   
   </appender>
   
   <logger name="com" level="DEBUG" additivity="false">
      <appender-ref ref="STDOUT" />
   </logger>
   
   <logger name="org.springframework.boot.autoconfigure" level="WARN">
      <appender-ref ref="STDOUT"/>
   </logger>
   
   <logger name="fileLogger" level="INFO" additivity="false">
      <appender-ref ref="FILE_APPENDER" />
   </logger>
   
   <root level="INFO">
      <appender-ref ref="STDOUT" />
   </root>
 
</configuration>
cs

 

 

[위와 같이 설정한 경우 Sample.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.sample.controller;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class Sample {
    
    protected static final Logger logger = LoggerFactory.getLogger(Sample.class);
    
    public static void main(String[] args) {
        
        logger.info("Sample.class logger print with info level");
        
        logger.error("fileLogger logger print with error level");
    }
 
}
 
cs

main 메소드를 실행시 logger.error 로 찍은 로그는 /logs/api.log 파일에 write 되어 있으며, 콘솔에선 찍히지 않는다.

 

[실행 결과 : 콘솔]

INFO : com.sample.controller.Sample - Sample.class logger print with info level

[실행 결과 : /logs/api.log 파일]

ERROR : fileLogger - fileLogger logger print with error level

 

2. 특정 로거에서 출력한 로그는 파일로 저장하기

기존의 console 로그 이외에 로거를 하나 추가로 두고,

추가한 로거를 사용하여 로그를 찍는 경우, 별도의 파일로 저장하고자 하는 경우

fileLogger 를 로거로 추가하고, 파일 어팬더를 주입.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%-5p: %c - %m%n" />
        </Console>
        
        <RollingFile name="logFile" fileName="/logs/api.log" filePattern="/logs/$${date:yyyyMM}/app_%d{yyyyMMdd}-%i.log.gz">
            <PatternLayout pattern="%-5p: %c - %m%n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
        </RollingFile>
    </Appenders>
    
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="console" />
        </Root>
        
        <Logger name="fileLogger" level="INFO" additivity="false">
             <AppenderRef ref="logFile" />
        </Logger>
    </Loggers>
    
</Configuration>
 
cs

 

Sample.class는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sample.controller;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class Sample {
    
    protected static final Logger logger = LoggerFactory.getLogger(Sample.class);
    protected static final Logger fileLogger = LoggerFactory.getLogger("fileLogger");
 
    public static void main(String[] args) {
        
        logger.info("Sample.class logger print with info level");
        
        fileLogger.info("fileLogger logger print with info level");
    }
 
}
 
cs

 

[실행결과 : 콘솔]

INFO : com.sample.controller.Sample - Sample.class logger print with info level

[실행결과 : /logs/api.log]

INFO : fileLogger - fileLogger logger print with info level

 

참고:

https://logging.apache.org/log4j/2.x/manual/architecture.html

https://logging.apache.org/log4j/2.x/manual/appenders.html

 

반응형

스프링 빈은 Thread safe 하나,

스프링 빈 내에서의 멤버변수는 Thread safe 하지 않다.

--> race condition 을 유의해야 한다.

--> stateless 하게 개발해야 한다.

 

※ race condition : 

두 개 이상의 프로세스가 공통 자원을 병행적으로(concurrently) 읽거나 쓸 때,

공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 그 실행 결과가 달라지는 상황을 말한다.

※ stateful :

상태 변화에 의존적인 상태를 의미.

ex)서버 session 은 stateful, 클라이언트가 토큰을 들고있는 방식은 stateless (공부중)

 

[Example]

1
2
3
4
5
6
7
8
9
10
11
@Service
public class TestServiceImpl implements TestService {
  
    private int idx = 0;
 
    @Override
    public void test(){
        System.out.println("idx : " + idx);
        idx++;
    }
}
cs

ControllerA  에서 TestService를 주입받아 test() 메소드 실행시 idx 값이 호출될 때마다 증가하며,

ControllerB 에서도 TestService를 주입받아 test() 메소드 실행시 idx 값이 호출될 때마다 증가함을 확인 할 수 있다.

 

만약 멤버변수를 단순 읽기 전용으로 사용한다면 문제되지 않지만, 위와 같이 읽기 뿐만 아닌 쓰는 용도로 사용한다면 문제가 된다.

 

이처럼 스프링 Bean 은 Default로 싱글톤 디자인패턴을 따르므로, 

멤버변수에 값을 저장(쓰는 행위)하는 식의 stateful한 설계는 피해야 한다.

(가급적 스프링 빈으로 관리되는 객체만 멤버변수로 사용하는게 좋다)

 

 

[코드 개선]

위와 같은 코드는 멤버변수를 제거하고, 메소드 내에서 지역변수로 선언 및 파라미터로 값을 들고 다니는 방식으로 수정을 하거나(권장), 빈의 scope 를 prototype 으로 주어 호출시마다 새로운 인스턴스를 생성하도록 해야 한다.

@Scope("prototype") annotation 을 class 선언 부에 달아주면 해당 객체는 싱글톤이 아닌 프로토타입 객체가 된다.

※ 싱글톤 빈 내에서 프로토타입 빈 생성을 할 경우 정상적으로 프로토타입 빈 동작이 되지 않는다(호출마다 빈이 생성되야 하나 그렇지 못함)

 

[prototype Scope를 적용한 코드]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@Scope("prototype")
public class TestServiceImpl implements TestService, InitializingBean, DisposableBean {
  
    private int idx = 0;
 
    @Override
    public void test(){
        System.out.println("idx : " + idx);
        idx++;
    }
 
    @Override 
    public void afterPropertiesSet() throws Exception { 
        logger.info("created service! "); 
    }
 
    @Override 
    public void destroy() throws Exception { 
        logger.info("destroyed service! "); 
    }
 
}
cs

※ InitializingBean, DisposableBean 인터페이스의 afterPropertiesSet(), destroy() 메소드는 빈이 생성, 제거 될 때 각각 호출된다.

 

코드를 위와 같이 수정한 후, 이전과 동일하게

1) ControllerA  에서 TestService를 주입받아 test() 메소드 실행,

2) ControllerB 에서도 TestService를 주입받아 test() 메소드 실행한 경우 결과는 아래와 같다.

[실행 결과]

created service!
idx : 0
created service!
idx : 1

 

참고:

토비의 스프링 3.1(도서)

https://gmlwjd9405.github.io/2018/11/10/spring-beans.html

https://beyondj2ee.wordpress.com/2013/02/28/%EB%A9%80%ED%8B%B0-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B9%88-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD/

https://gmlwjd9405.github.io/2018/11/10/spring-beans.html

 

 

반응형

원래 객체의 기능을 다른 객체가 대신 처리하도록 설계하는 패턴

AOP 등에 사용되는 패턴

 

 

Proxy 기본 패턴

[Subject interface]

1
2
3
public interface Subject {
    public String action();
}
cs

 

[RealSubject class]

실제 사용하는 객체

1
2
3
4
5
6
7
8
public class RealSubject implements Subject{
 
    @Override
    public String action() {
        return "Real Subject action()";
    }
}
 
cs

 

[Proxy class]

프록시 객체.

내부적으로 RealSubject 객체를 생성하여 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Proxy implements Subject {
    
    RealSubject realSubject;
    
    @Override
    public String action() {
        if(realSubject == null) {
            realSubject = new RealSubject();
        }
        return realSubject.action();
    }
}
 
cs

 

[Client class]

1
2
3
4
5
6
7
8
9
public class Client {
 
    public static void main(String[] args) {
        Subject subject = new Proxy();
        System.out.println(subject.action());
    }
 
}
 
cs

 

 

Proxy의 실제 사용

[AOP 흉내내기]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Proxy implements Subject {
    
    RealSubject realSubject;
    
    @Override
    public Object action() {
        if(realSubject == null) {
            realSubject = new RealSubject();
        }
        
        preProcess();
        Object result = realSubject.action();
        postProcess();
        
        return result;
    }
    
    private void preProcess() {
        System.out.println("선행작업");
    }
    
    private void postProcess() {
        System.out.println("사후작업");
    }
}
 
cs

 

[실제 AOP 소스]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Component
@Aspect
public class ControllerAOP {
 
    protected static final Logger logger = LoggerFactory.getLogger(ControllerAOP.class);
    private final String CANNOT_PRINT_INPARAM = "{IN_PARAMS LENGTH IS TOO LONG TO PRINT}";
    private final String CANNOT_PRINT_OUTPARAM = "{OUT_PARAMS LENGTH IS TOO LONG TO PRINT}";
    private final String CANNOT_PRINT_VALUE = "VALUE LENGTH IS TOO LONG TO PRINT";
    private final String NOTHING_TO_PRINT = "RETURN TYPE IS VOID";
    
    @Around("execution(public void com.sample..*Controller.*(..))")
    public void voidAround(ProceedingJoinPoint pjp) {
        
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
 
        String inputParam = null;
        
        for (Object obj : pjp.getArgs()) {
            if (obj instanceof Map) {
                inputParam = obj.toString();
            } else if (obj instanceof String){
                inputParam = (String)obj;
            }
        }
 
        long start = System.currentTimeMillis();
 
        String controller = (String) pjp.getTarget().getClass().getSimpleName();
 
        String path = request.getRequestURI();
        String addr = request.getRemoteAddr();
        int port = request.getRemotePort();
 
        logger.info("##########################################################################");
        logger.info("# REQUEST | CONTROLLER = {} | METHOD = {} | REMOTEADDR = {} | PORT = {} | IN_PARAMS = {}",
                controller, path, addr, port, inputParam==null?"":(inputParam.length()>=1500?this.CANNOT_PRINT_INPARAM:inputParam));
        logger.info("##########################################################################");
        
        String result_msg = "success";
        
        try {
            pjp.proceed();
            result_msg = "success";
        } catch (APIException e) {
            result_msg = "fail";
        } catch (Throwable e) {
            result_msg = "fail";
        } finally {
            long end = System.currentTimeMillis();
            logger.info("##########################################################################");
            logger.info("# RESPONSE | CONTROLLER = {} | METHOD = {} | RESULT = {} | REMOTEADDR = {} | PORT = {} | TIME = {} ms | IN_PARAMS = {} | OUT_PARAMS = {}"
                    controller, path, result_msg, addr, port, end-start, inputParam==null?"":(inputParam.length()>=1500?this.CANNOT_PRINT_INPARAM:inputParam), 
                    this.NOTHING_TO_PRINT);
            logger.info("##########################################################################");
        }
 
 
    }
cs

위 AOP 코드 요약 : joinpoint(클라이언트에서 호출되는 모든 메소드) 중에서 pointcut으로(execution) 잡은 특정 메소드만 잡아 공통 관심사로 (advice(@Around)) 로 처리.

 

실제 호출되는 메소드를 인자(ProceedingJoinPoint)로 받아 프록시 패턴과 같이 처리한다.

pjp.proceed() 가 실제 메소드 실행부이며,

pjp.proceed() 위 아래로 로그를 처리하는 코드가 있다.

 

 

참고 :

도서 Head first design patterns,

https://effectiveprogramming.tistory.com/entry/Proxy-%ED%8C%A8%ED%84%B4%EA%B3%BC-%EA%B7%B8-%ED%99%9C%EC%9A%A9

 

반응형

 

https://velopert.com/2350

 

https://mangkyu.tistory.com/55?category=761303

 

https://mangkyu.tistory.com/56

 

https://www.sauru.so/blog/basic-of-oauth2-and-jwt/

반응형

1. component-scan

- spring 2.5 버전에 출시

- 2.5 버전 이전엔 xml file 에 모든 bean 을 설정해야 했는데, component-scan 이 xml 내의 bean 설정 양을 줄여줬다

- @Component, @Repository, @Service, @Controller 어노테이션을 스캔한다

- @Autowired, @Qualifier 어노테이션도 해석

- component-scan 이 선언되어있다면 annotation-config 는 필요없음

 

2.annotation-driven

- annotation-driven 은 spring mvc 컴포넌트를 활성화

- annotation-driven 을 포함하지 않아도 component-scan을 사용하여 Bean을 생성한 경우 mvc application은 동작

- annotation-driven은 구성되지 않은 특정 빈을 구성하는데 추가 작업을 수행

- @Controller 에 요청을 하기 위해 필요한 HandlerMapping, HandlerAdapter 을 등록

- @Number로 숫자 필드 지원 , classPath에 Jackson이 있는 경우 JSON 쓰기 읽기를 지원 와 같은 작업 적용

 

3. annotation-config

- 어플리케이션 context에 이미 등록되어 있는 (xml에 선언돼있거나 package scanning 으로 검색된) 어노테이션 빈을 활성화

- 스프링 컨텍스트에 의해 생성 및 저장된 빈에 대해 @Autowired, @Qualifier 어노테이션을 해석

- component-scan 또한 같은 일을 하지만, annotation-config 는 빈을 찾아 등록해주진 않는다. 이미 등록이 끝난 bean 들을 활성화할 할 뿐이다.

 

※ annotation-config 만 설정에 잡혀있을 경우, Bean 이 스캔에 의해 자동으로 등록되지 않으므로 xml 에 Bean 설정을 해주어야 함. 현재 개발중인 배치서버 설정에 빈이 자동으로 잡히지 않아 찾아보니 annotation-config 설정만 잡혀있고, component-scan 혹은 annotation-driven 은 잡혀있지 않았음.

 

참고 및 해석 : https://javabeat.net/spring-mvc-component-scan-annotations/ 

반응형

1. #{}

#{}사용시 mybatis 가 preparedStatement 를 생성하고, preparedStatement 매개변수 #{?} 값을 안전하게 셋팅하게 한다.

#{param} 값을 'param' 과 같이 ''로 감싸며 치환한다.

SQL injection 과 같은 공격을 방지할 수 있다. 더 빠르고, 안전하다.

 

1
2
3
4
5
SELECT *
FROM user_info
WHERE 1=1
AND id = #{id}    -- #{id} >> 'developyo@tistory.com'
AND pw = #{pw}    -- #{pw} >> '1234'
cs

 

2. ${}

${} 사용시 ${param} 값을 param 과 같이 '' 로 감싸지 않은채 치환한다.

SQL injection 과 같은 공격을 받을 수 있다.

직접적인 값 셋팅(ORDER BY 절의 기준 칼럼을 동적으로 지정하고 싶은 경우 등)이 필요한 경우 사용할 수 있다.

1
2
3
SELECT *
FROM user_info
ORDER BY ${columnName} -- ${columnName} >> join_date
cs

 

 

[ SQL injection 공격의 예 ]

1. #{} 사용

id 파라미터로 developyo@tistory.com

pw 파라미터로 '1' OR 1=1 

1
2
3
4
5
SELECT *
FROM user_info
WHERE 1=1
AND id = 'developyo@tistory.com'
AND pw = ''1' OR 1=1'
cs

: sql Exception 발생

 

2. ${} 사용

id 파라미터로 'developyo@tistory.com'

pw 파라미터로 '1' OR 1=1 

1
2
3
4
5
SELECT *
FROM user_info
WHERE 1=1
AND id = 'developyo@tistory.com'
AND pw = '1' OR 1=1
cs

: 조회 성공

 

※ 만약 ${}을 사용할 경우 SQL injection 공격을 막기위해 이스케이프 처리가 필요하다.

 

참고 : https://stackoverflow.com/questions/39954300/when-to-use-vs

반응형

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다. (Head First Design Pattern 내용 발췌)

Subject(주제) 객체에 변화가 일어날 경우 Subject에 의존하고 있는 다수의 Observer 에 변화를 알리는 일대다 의존성을 갖는 디자인 패턴. 대표적으로 java.awt 의 listener가 Observer pattern 이라고 할 수 있다.

 

Observer pattern 을 파악하기 위해

java.awt 의 lister 와 Button 을 어설프게나마 직접 구현해보자.

 

[Listener]

Observer 와 같은 역할을 하는 Listener Interface

1
2
3
4
5
6
package design_pattern.observer;
 
public interface Listener {
    public void performAction();
}
 
cs

 

[CustomListener1]

Observer 를 구현한 CustomListener1

performAction() 은 주제 객체의 변화가 일어났을 때 호출이 되는 메소드

1
2
3
4
5
6
7
8
9
package design_pattern.observer;
 
public class CustomListener1 implements Listener {
    @Override
    public void performAction() {
        System.out.println("custom listener1 perform action !");
    }
}
 
cs

 

[Component]

주제 인터페이스 Component

1
2
3
4
5
6
7
8
package design_pattern.observer;
 
public interface Component {
    public void addListener(Listener l);
    public void removeListener(Listener l);
    public void notifyListeners();
}
 
cs

 

[Button]

주제 인터페이스를 구현한 주제 객체 Button

Observer 역할을 하는 listeners 를 멤버변수로 갖고 있다(구상)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package design_pattern.observer;
 
import java.util.ArrayList;
 
public class Button implements Component {
    
    private ArrayList<Listener> listeners;
    
    public Button() {
        this.listeners = new ArrayList<Listener>();
    }
    
    @Override
    public void addListener(Listener l) {
        listeners.add(l);
        System.out.println("listener added!");
    }
 
    @Override
    public void removeListener(Listener l) {
        int idx = listeners.indexOf(l);
        listeners.remove(idx);
        System.out.println("listener removed!");
    }
 
    @Override
    public void notifyListeners() {
        for(Listener l : listeners) {
            l.performAction();
        }
    }
    
    public void buttonClicked() {
        notifyListeners();
    }
    
}
 
cs

 

[Main]

Listener 를 익명클래스로 구현 후 customListeners2 변수로 선언하였다.

Listener(Observer) 두개를 Button(주제) 객체에 등록(addListener)해주었고,

실제 Button 을 클릭 해 볼 수 없으니,

마치 버튼을 클릭 한 것 처럼 버튼객체의 buttonClicked() 메소드를 호출해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package design_pattern.observer;
 
import java.awt.TextField;
import java.awt.event.ActionListener;
 
public class Main {
 
    public static void main(String[] args) {
        
        Button btn = new Button();
        
        Listener customListener1 = new CustomListener1();
        
        Listener customListener2 = new Listener() {
            @Override
            public void performAction() {
                System.out.println("custom listener2 perform action !");
            }
        };
        
        btn.addListener(customListener1);
        btn.addListener(customListener2);
        
        btn.buttonClicked();
        
        System.out.println("----------------------------");
        
        btn.removeListener(customListener2);
        
        System.out.println("----------------------------");
        btn.buttonClicked();
        
    }
    
}
 
cs

 

실행 결과

listener added!
listener added!
custom listener1 perform action !
custom listener2 perform action !
----------------------------
listener removed!
----------------------------
custom listener1 perform action !

 

반응형

"어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스 제공.

퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할 수 있다."

 

사용할 메소드가 여러 클래스에 퍼져 있는 경우

필요한 클래스들을 모두 생성자로 생성하여 필요한 메소드를 각각 호출 하는 대신,

(최소 지식 원칙 (의존하는 클래스의 갯수를 최대한 적게) 위배)

별도의 클래스(퍼사드)에 사용할 클래스들을 멤버변수로 담고,

사용하고자 하는 멤버변수의 메소드들을 한곳에 묶은 새로운 메소드를 작성한다.

 

예를 들어, 메일 전송 로직을 다음과 같이 구현한다고 하자.

1. 수신인의 메일주소를 확인한다.

2. 메일을 전송한다.

3. 메일 전송 결과를 이력에 남긴다.

위 기능들이 각각 다른 클래스에 존재하는 메소드라고 한다면

위 세 기능을 묶은 하나의 메소드를 작성하여, 메인시스템에선 해당 메소드(서브시스템)만 호출한다.

 

[ValidationService class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package design_pattern.fasade;
 
public class ValidationService {
    
    String mailAddr = "";
    
    public ValidationService(String addr){
        this.mailAddr = addr;
    }
    
    public boolean addrChk() {
        if(!mailAddr.isEmpty()) {
            System.out.println("mail validation check success!");
            return true;
        }
        return false;
    }
    
}
 
cs

 

[MailService class]

1
2
3
4
5
6
7
8
package design_pattern.fasade;
 
public class MailService {
    public void mailSend() {
        System.out.println("mail has been sent");
    }
}
 
cs

 

[HstService class]

1
2
3
4
5
6
7
8
package design_pattern.fasade;
 
public class HstService {
    public void hstSave() {
        System.out.println("sent mail saved");
    }
}
 
cs

 

[Facade.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package design_pattern.fasade;
 
public class Facade {
    private ValidationService vs;
    private MailService ms;
    private HstService hs;
    
    public Facade(ValidationService vs, MailService ms, HstService hs) {
        this.vs = vs;
        this.ms = ms;
        this.hs = hs;
    }
    
    public void facadeFunc() {
        if(vs.addrChk()) {
            ms.mailSend();
        } 
        hs.hstSave();
    }
}
 
cs

주시스템에서 사용하고자 하는 서브시스템들을 멤버변수로.

멤버변수 내에서 사용할 메소드들을 따로 모은, 새로운 메소드 구현(facadeFunc())

 

[main]

1
2
3
4
5
6
7
8
9
10
11
12
13
package design_pattern.fasade;
 
public class Main {
    
    public static void main(String args[]) {
        Facade f = new Facade(new ValidationService("receiver@gmail.com")
                            , new MailService()
                            , new HstService());
        f.facadeFunc();
    }
    
}
 
cs

 

디자인 패턴 중 가장 단순한(이해하기 쉬운..) 구조이며 가장 많이 쓰이는 패턴 중 하나

반응형

DB연동 및 mybatis 설정이 끝났다면

junit 을 이용하여 설정이 잘 되었는지 확인을 해보자.

 

1. Test 코드 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.jpp;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
 
import com.jpp.main.config.MybatisConfig;
import com.jpp.main.dao.MainDAO;
import com.jpp.main.vo.UserVO;
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class MainServiceTest {
    
    @Autowired
    private MainDAO mainDao;
    
    @Test
    public void getUserList() throws Exception {
        
        for(UserVO vo : mainDao.getUserList()) {
            System.out.println(vo.getName());
        }
        
    }
}
 
cs

Spring Framework 에서 Bean 을 가져오기위해 @ContextConfiguration(classes = ~) 을 사용했지만 

Spring Boot 는 @*Test(@SpringBootTest 를 포함한 각종 Test 어노테이션) 사용시 Bean 을 알아서 가져온다고 한다.

참고 및 부분 발췌

※JUnit 4 사용시 @RunWith(SpringRunner.class) 를 꼭 달아주어야 한다.

※ @SpringBootTest(classes = {MainDAO.class, MybatisConfig.class}) 와 같이 테스트에 사용할 특정 Bean 을 지정해 줄 수 도 있다.

※ ApplicationContext (스프링컨텍스트) 에서 DAO bean 을 .getBean으로 가져와 테스트 하는 것 또한 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.jpp;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
 
import com.jpp.main.config.MybatisConfig;
import com.jpp.main.dao.MainDAO;
import com.jpp.main.vo.UserVO;
 
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MainDAO.class, MybatisConfig.class})
public class MainServiceTest {
    
    //@Autowired
    //private MainDAO mainDao;
    
    @Autowired
    private ApplicationContext context;
    
    @Test
    public void getUserList() throws Exception {
        
        MainDAO dao = context.getBean("mainDAO", MainDAO.class);
        
        for(UserVO vo : dao.getUserList()) {
            System.out.println(vo.getName());
        }
    }
}
 
cs

 

2. Run as JUnit Test 로 실행.

결과 :

 

콘솔 결과 :

 

만약 테스트를 기존에 사용하는 DB가 아닌 별도의 DB 에서 하고자 하는 경우는 어떻게 해야할까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.jpp;
 
import javax.sql.DataSource;
 
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
import com.jpp.main.dao.MainDAO;
import com.jpp.main.vo.UserVO;
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class MainServiceTest {
    
    @Autowired
    private MainDAO mainDao;
    
//    @Autowired
//    private ApplicationContext context;
    
    @Test
    public void getUserList() throws Exception {
        
        //MainDAO dao = context.getBean("mainDAO", MainDAO.class);
        
        for(UserVO vo : mainDao.getUserList()) {
            System.out.println(vo.getName());
        }
    }
    
    @TestConfiguration
    @MapperScan(value= {"com.jpp.main.mapper"})
    @EnableTransactionManagement
    public static class TestConfig {
 
            @Bean
            public DataSource customDataSource() {
                return DataSourceBuilder.create()
                                        .url("jdbc:mysql://ip:3306/~?useSSL=false&serverTimezone=UTC")
                                        .driverClassName("com.mysql.cj.jdbc.Driver")
                                        .username("id")
                                        .password("pw")
                                        .build();
            }
            
            @Bean
            public SqlSessionFactory sqlSessionFactory(DataSource dataSource)throws Exception{
                    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
                    sessionFactory.setDataSource(dataSource);
                    
                    Resource[] res = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml");
                    
                    sessionFactory.setMapperLocations(res);
                    
                    return sessionFactory.getObject();
            }
 
            @Bean
            public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
                return new SqlSessionTemplate(sqlSessionFactory);
            }
    }
}
 
cs

위와 같이 @TestConfiguration 을 테스트 코드 내부에 inner Class로 작성하여 테스트만을 위한 DB 설정을 따로 해줄 수 있다.

테스트 코드 내부에 inner Class 로 @TestConfiguration 작성시 별다른 설정이 없어도 @SpringBootTest 가 해당 config Bean을 인식하여 DI 한다.

inner Class가 아닌 외부에 TestConfig.java 파일을 따로 작성해줄 경우 @Import(TestConfig.class) 어노테이션을 @SpringBootTest 와 함께 달아주어야 테스트 코드에서 주입 및 사용이 가능하다.

ex)

@RunWith(SpringRunner.class)
@SpringBootTest
@Import(TestConfig.class)

 

그럼 이제 위의 테스트 코드를 실행해 보자.

아래와 같은 에러가 발생한다.

 

The bean 'customDataSource', defined in com.jpp.MainServiceTest$TestConfig, could not be registered. A bean with that name has already been defined in class path resource [com/jpp/main/config/MybatisConfig.class] and overriding is disabled.

 

동일한 이름을 가진 Bean이 MybatisConfig.class에 이미 정의되어 있어 TestConfiguration 으로 작성한 Bean 을 등록할 수 없단다.

 

※ 동일한 Bean 의 이름이 존재할 경우 IoC 컨테이너가 Test 코드 내의 TestConfiguration 어노테이션이 달린 Bean 을 우선적으로 인식하여 주입해줄까 싶었는데 그것 까진 안되나보다.

27번라인을 아래와 같이 고쳐주자

@SpringBootTest(classes = {TestConfig.class, MainDAO.class})

: @SpringBootTest 어노테이션에 테스트 DB가 설정되어있는 Bean class와 이를 사용할 class 를 명시

 

다시 테스트 모드로 실행시켜보면 결과는 성공일 것이다.

 

 

※ Test용 Config 를 외부 클래스로 작성하고 테스트코드에서 사용하고자 할 경우

1. 앞서 언급한 @Import 를 사용하는 대신

2. @SpringBootTest(classes = {TestConfig.class}) 와 같이 테스트용 Config 를 명시하여 사용 할 수 도 있다.

   (@Import 를 사용하지 않아도 주입 받아 사용이 가능)

 

※ 단, 위처럼 프로젝트 내에 동일한 이름의 Bean 이 존재할 경우(2개의 DataSource Bean 이 동일한 이름으로 존재) @SpringBootTest(classes = {TestConfig.class, MainDAO.class})와 같이 테스트에 사용할 Bean을 명시하여 사용해야한다. (@Import만 사용시 앞서 살펴보았던 에러와 동일하게, Bean 이 중복 선언된다는 Exception이 발생한다)

 

 

* Controller를 테스트 하는 경우 아래와 같이 가능하다.

1. 테스트 대상 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.jpp.main.controller;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
 
import com.jpp.main.service.MainService;
import com.jpp.main.vo.UserVO;
 
@Controller
public class MainController {
    
    @Autowired
    private MainService mainService;
    
    @PostMapping("/api2")
    @ResponseBody
    public String api(@RequestBody String test) throws Exception {
        String rs = "good";
        
        System.out.println("api2 called !");
        
        return "{\"good\":\"res\"}";
    }
}
 
cs

 

 

2. test class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.jpp;
 
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
 
import com.jpp.main.controller.MainController;
import com.jpp.main.service.MainService;
 
@RunWith(SpringRunner.class)
@WebMvcTest(MainController.class)
public class MainServiceTest2 {
    
    @Autowired
    private MockMvc mvc;
    
    @MockBean
    private MainService mainService;
    
    @Test
    public void api() throws Exception {
        
        
        ResultActions actions = 
                mvc.perform(post("/api2")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content("{\"test\":\"val\"}")
                        ).andDo(print());
        
        actions.andExpect(status().isOk());
    }
    
}
 
cs

 

3. junit 실행 결과

 

 

JUnit 과 관련된 트랜잭션(@Transactional) 부분, Mock 객체 등에 대한 내용은 추가 공부 및 정리 후 작성 하겠다.

 

 

Spring Boot의 Test코드에 대한 설명이 매우 잘 되어 있는 글

 

반응형

+ Recent posts