Thread를 구현할 때 Runnable 인터페이스를 구현하거나 Thread 클래스를 상속하여 구현한다. 구현한 Thread를 new MyThread().start() 와 같이 호출하여 직접적으로 실행할 수도 있지만 기본 JDK의 java.util.concurrent패키지의 ExcecutorService를 이용하여 실행할 수도 있다.


ExecutorService

ExecutorService는 재사용이 가능한 ThreadPool로 Executor 인터페이스를 확장하여 Thread의 라이프사이클을 제어한다.
Thread를 활용하여 다수의 작업(Task)들을 비동기로 수행한다는 것은 간단하지 않다. Thread의 라이프사이클(생성과 제거 등)이나 발생할 수 있는 여러가지 low level의 고려사항들이 존재하는데 이를 개발자가 신경쓰지 않도록 편리하게 추상화한 것이 ExecutorService이다.

ExecutorService에 Task(작업)를 지정해주면 가진 ThreadPool을 이용하여 Task를 실행한다. Task는 큐(Queue)로 관리되기 때문에 ThreadPool의 Thread 갯수보다 실행할 Task가 많은경우 미실행된 Task는 큐에 저장되어 실행을 마친 Thread가 생길 때까지 기다린다.

 


ExecutorService 초기화

ExecutorService를 초기화 하는 방법에는 2 가지 방법이 있다.

1. 직접 new 키워드를 사용

ExecutorService는 인터페이스이기 때문에 구현체인 ThreadPoolExecutor를 new키워드로 초기화한다. (필요에 따라 다른 구현체를 초기화해도 된다.)

아래의 초기화 코드에서 10개의 core thread, 10개의 max thread, 0 밀리세크의 keepAliveTime, 작업 큐로는 LinkedBlockingQueue가 초기화되었다. Task(작업)을 위한 Queue에는 Runnable과 Callable 인터페이스를 구현한 클래스를 받을 수 있는데 return값이 있냐(Callable) 없냐(Runnable)에 따라 선택하면 된다.

ExecutorService executorService = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());

2. Executors 클래스에서 제공하는 Factory method를 사용

제공되는 3가지의 factory method를 이용한 초기화이다. 메서드명에서 생성되는 ThreadPool의 성향을 유추할 수 있으며 실행하고자 하는 Task에 따라 선택하여 사용한다.

// 1. 10개 고정 사이즈의 ThreadPool 생성
ExecutorService executorService = Executors.newFixedThreadPool(10);

// 2. 1개 고정 사이즈의 ThreadPool 생성
ExecutorService executorService = Executors.newSingleThreadExecutor();

// 3. 유동적으로 증가하고 줄어드는 ThreadPool 생성
ExecutorService executorService = Executors.newCachedThreadPool();

new 키워드를 사용하는 것이 좀 더 세부적인 설정이 가능하지만 Executors를 사용하는 것이 더 간편하다. 대부분의 경우 간편한 설정으로 원하는 작업이 가능하다.


ExecutorService Task 할당

ExecutorService를 초기화 했다면 ThreadPool에 원하는 Task(작업)을 할당해야 한다. 일단 Task를 Callable / Runnable 인터페이스를 구현하여 생성하고, ExecutorService의 메서드를 호출하여 실행한다.

// Runnable 인터페이스로 Task 정의
Runnable runnableTask = () -> {
	try{
		System.out.println(Thread.currentThread().getName() + " start");
		TimeUnit.MILLISECONDS.sleep(500);
		System.out.println(Thread.currentThread().getName() + " end");
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
};

// Callable 인터페이스로 Task 정의
Callable callableTask = () -> {
	TimeUnit.MILLISECONDS.sleep(5000);
	return "Task's execution";
};

// 정의한 Task를 List에 추가
List<Callable> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);

 

아래는 위에서 정의한 작업(Task)을 할당(실행)하기 위해서 제공되는 메서드들이다.

 

1. execute() : 리턴타입이 void로 Task의 실행결과나 Task의 상태(실행중 or 실행완료)를 알 수 없다.

executorService.execute(runnableTask);

 

2. submit() : Task를 할당하고 Future 타입의 결과값을 받는다. 결과가 리턴되어야 하므로 주로 Callable을 구현한 Task를 인자로 준다.

Future future = executorService.submit(callableTask);

 

3. invokeAny() : Task를 Collection에 넣어서 인자로 넘겨줄 수 있다. 실행에 성공한 Task 중 하나의 리턴값을 반환한다.

String result = executorService.invokeAny(callableTasks);

 

4. invokeAll() : Task를 Collection에 넣어서 인자로 넘겨줄 수 있다. 모든 Task의 리턴값을 List<Future<>> 타입으로 반환한다.

List<Future> futures = executorService.invokeAll(callableTasks);

ExcecutorService 종료

실행 명령한 Task가 모두 수행되어도 ExecutorService는 자동으로 종료되지 않는다. 앞으로 들어올 Task를 처리하기 위해 Thread는 wait 상태로 대기한다. 그러므로 종료를 위해서는 제공되는 shutdown() 이나 shutdownNow() API를 사용해야 한다.

 

1. executorService.shutdown()

실행중인 모든 Task가 수행되면 종료한다.

 

2. List<Runnable> notExecutedTasks = executorService.shutDownNow()

실행중인 Thread들을 즉시 종료시키려고 하지만 모든 Thread가 동시에 종료되는 것을 보장하지는 않고 실행되지 않은 Task를 반환한다.

여기에 추가로 두 개의 shutdown 메서드가 결합된 awaitTermination()을 사용하는 것이 추천된다. 이 메서드는 먼저 새로운 Task가 실행되는 것을 막고, 일정 시간동안 실행중인 Task가 완료되기를 기다린다. 만일 일정 시간동안 처리되지 않은 Task에 대해서는 강제로 종료시킨다.

executorService.shutdown();
try {
	if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
		executorService.shutdownNow();
	} 
} catch (InterruptedException e) {
	executorService.shutdownNow();
}

Future 인터페이스

submit()invokeAll() 메서드를 호출할때 반환하는 Future 객체로 Task의 결과값이나 상태(실행중 또는 실행완료)를 알 수 있다. 또한 Future 인터페이스는 Blocking method인 get()을 제공하는데 Task 실행 결과를 얻을 수 있다.(Runnable을 구현한 Task라면 null이 반환된다.)

Blocking이기 때문에 실행중에 get()이 호출되는 경우 실행이 끝날 때까지 대기한다. 이는 성능저하를 불러올 수 있으므로 Timeout을 설정하여 일정 시간이 지나면 TimeoutException이 발생하도록 유도할 수 있다.

Future<String> future = executorService.submit(callableTask);
String result = null;
try {
    result = future.get(); // Task가 실행중이면 여기서 대기한다.
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

// Timeout 설정, 지정된 시간이 지나면 TimeoutException이 발생한다.
String result = future.get(200, TimeUnit.MILLISECONDS); 

이 밖에도 isDone(), cancel(), isCancelled() 메서드가 있다.

boolean isDone = future.isDone(); // Task가 실행되었는지?
boolean canceled = future.cancel(true); // Task를 취소
boolean isCancelled = future.isCancelled(); // Task가 취소되었는지?

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

equals, hashcode  (0) 2020.12.07
JVM, JRE, JDK 간단 개념  (0) 2020.12.07
Comparator, Comparable 어떻게 사용할까?  (0) 2020.11.29
LocalDateTime, ZonedDateTime  (0) 2019.10.11
Collections를 이용한 정렬(sort method) 활용  (0) 2017.05.09

자바에서 정렬에 관련된 작업을 할 경우 떠올릴 수 있는 대표적인 인터페이스가 두가지 있다.

Comparator와 Comparable이다.

두 가지 인터페이스의 차이점과 사용법에 대해 알아보자.

 

두 인터페이스가 사용되는 이유는 공통적이다.

Primitive 타입의 숫자나 문자를 비교할 경우 명확한 기본 비교로직이 존재한다. 오름차순이라면 '1'보단 '2'가 크고, '가' 보다는 '나'가 크다. 하지만 비교하는 대상이 객체라면 기본 비교로직으로는 비교가 불가능하다.

예를들어 Game 클래스로 만든 객체들을 정렬한다고 가정한다면 여러가지 정렬을 위한 기준이 있을 수 있다. (발매년도, 타이틀명 등등) 이 때, 정렬 기준에 대한 명확한 로직을 정의할 수 있도록 도와주는 것이 Comparator와 Comparable 인터페이스이다. 

 

하나씩 살펴보자.

 

Comparator

'비교기'라는 이름에서 알 수 있듯이 비교를 위한 보조로직을 따로 작성하여 정렬시 인자로 건네준다.

보조로직 작성은 Comparator의 int compare(obejct args1, object args2) 메서드에 작성한다.

리스트의 정렬은 Collections.sort() 메서드를 사용하는데, 여기의 인자로 전달한다.

public class GameComparatorTest {
    @Test
    public void test1(){
        Comparator<Game> gameComparator = new Comparator<Game>() {
            // 비교를 위한 로직을 작성
            public int compare(Game game1, Game game2) {
                // 발매년도는 내림차순으로 하면서, 같은 발매년도에서는 타이틀명을 오름차순으로 하고싶다.
                if(game1.year != game2.year){
                    return Integer.compare(game1.year, game2.year) * (-1);
                }else {
                    return game1.title.compareTo(game2.title);
                }
            }
        };
 
        List<Game> games = new ArrayList<>();
        games.add(new Game(2000"Assassin Creed"));
        games.add(new Game(1990"Overwatch"));
        games.add(new Game(2000"League Of Legends"));
        games.add(new Game(1990"A Power"));
 
        Collections.sort(games, gameComparator);
 
        System.out.println(games);
    }
}
 
class Game {
    int year;
    String title;
 
    public Game(int year, String title) {
        this.year = year;
        this.title = title;
    }
 
    @Override
    public String toString() {
        return "Game{" +
                "year=" + year +
                ", title='" + title + '\'' +
                '}';
    }
}

// 출력결과

[Game{year=2000, title='Assassin Creed'}, Game{year=2000, title='League Of Legends'}, Game{year=1990, title='A Power'}, Game{year=1990, title='Overwatch'}]

발매년도로 내림차순 되었고, 발매년도가 같은경우 타이틀명으로 오름차순 되었다. 

 

 

Comparable

Comparator와 유사하게 비교를 위한 보조로직을 정의하지만 작성하는 위치가 다르다. '비교할 수 있는'이라는 뜻과 같이 비교하고자 하는 클래스에 비교로직을 정의한다. 마찬가지로 Collections.sort() 메서드로 정렬할 수 있고 이 때는 두번째 인자가 필요하지 않는다.

public class GameComparableTest {
    @Test
    public void test1(){
        List<Game> games = new ArrayList<>();
        games.add(new Game(2000"Assassin Creed"));
        games.add(new Game(1990"Overwatch"));
        games.add(new Game(2000"League Of Legends"));
        games.add(new Game(1990"A Power"));
 
        Collections.sort(games);
 
        System.out.println(games);
    }
}
 
class Game implements Comparable<Game> {
    int year;
    String title;
 
    public Game(int year, String title) {
        this.year = year;
        this.title = title;
    }
 
    @Override
    public String toString() {
        return "Game{" +
                "year=" + year +
                ", title='" + title + '\'' +
                '}';
    }
 
    @Override
    public int compareTo(Game game1) {
        // 발매년도는 내림차순으로 하면서, 같은 발매년도에서는 타이틀명을 오름차순으로 하고싶다.
        if(this.year != game1.year){
            return Integer.compare(this.year, game1.year) * (-1);
        }else {
            return this.title.compareTo(game1.title);
        }
    }
}

출력결과는 Comparator의 출력결과와 동일하다.

 

즉, 두 가지 인터페이스 모두 객체 비교를 위한 보조로직을 정의하는데 사용되는 인터페이스이다. 상황에 맞게 선택하여 사용하자.

 

 

 

 

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

JVM, JRE, JDK 간단 개념  (0) 2020.12.07
ExecutorService 사용법  (0) 2020.12.01
LocalDateTime, ZonedDateTime  (0) 2019.10.11
Collections를 이용한 정렬(sort method) 활용  (0) 2017.05.09
BigDecimal & BigInteger  (0) 2017.04.18

목표

  • Maven에 대한 기본개념 이해
  • Maven의 의존성 관리 빌드 자동화 이해

Maven

의존성 관리와 빌드 자동화 기능을 제공하는 툴이며, 프로젝트 내에서 pom.xml을 주설정파일로 하여 동작한다.


의존성 관리

개발시 사용하는 라이브러리에 대해 pom.xml을 기준으로 메이븐 저장소로부터 다운받아 개발자의 프로젝트에 추가해 준다. (로컬저장소의 .m2 폴더에 다운로드 되며 가끔 의존성이 꼬일경우 .m2 폴더 내용을 날려주면 해결되는 경우가 있었다.)

라이브러리 추가를 위해서는 <dependencies/> 이하에 <dependency/> 태그에 작성하며 groupId, artifactId, version 세가지 정보가 필요하다. 보통 maven repository 웹사이트에서 라이브러리를 검색하여 등록한다,

dependency에 <scope>의 경우 compile, runtime, provided, test등이 올 수 있는데 해당 라이브러리가 언제 필요한지, 언제 제외되는지를 나타낸다.


빌드 자동화

메이븐은 자바 프로젝트의 빌드(Build)를 자동화해 주는 빌드 툴(Build Tool)이다. 즉, 자바 소스를 컴파일하고 패키징하여 배포까지 자동으로 해주는 도구이다.

컴파일->패키징->배포 과정(maven build)에 대한 세부 설정값들은 pom.xml에 작성한다.

 

maven build는 LifeCycle이 존재하며, 메이븐에 내장된 라이프사이클은 default, clean, site 3가지가 있다.

라이프사이클은 세부적으로 페이즈(phase)로 구성되어 있다. 예를 들어 default 라이프사이클은 다음과 같이 7개의 페이즈로 구성되며(전부는 아니고 큰 줄기로하면) 이들 사이에는 실행순서가 존재한다.

mvn install 명령어로 install 페이즈를 실행하면 1번 validate 페이즈부터 6번 install 페이즈까지 순차적으로 실행되는 구조다. (deploy는 실행되지 않는다.)

  1. validate
  2. compile
  3. test
  4. package
  5. verify
  6. install
  7. deploy

mvn 명령은 스페이스를 구분자로 여러동작을 한번에 지정할 수 있다.

mvn clean deploy : clean 페이즈 실행 후 1번 페이즈 validate부터 7번 페이즈 deploy까지 실행한다.

 

위의 Build Phase는 다시 Plugin Goal의 모음으로 이루어져 있다. Phase는 구체적인 작업을 나타내지는 않는다. 예를들어 package는 패키징된 결과물이 jar 파일이냐 war 파일이냐에 따라 다르게 수행되어야 하지만 Phase는 세부정보를 표시하지 않는 상위 개념이다.

pom.xml의 설정에 따라서 Phase마다 수행되는 세부작업을 Goal이라 부르고, Goal들의 집합을 Plugin이라 한다. 각 Phase마다 수행되어야할 Goal들이 맵핑되어 있고, Phase를 실행하면 설정에 맞는 Goal이 수행되는 구조다.

Goal은 하나 이상의 Phase에 맵핑되어 있거나, 맵핑이 되어 있지 않은 것도 있는데 맵핑된 Phase가 없다면 해당 Goal을 직접적으로 호출하여 실행할 수 있다.

 

mvn clean dependency:copy-dependencies package

  1. clean phase 실행(라이프사이클 순서에 따라 순차적으로)
  2. dependency:copy-dependencies : dependency plugin의 copy-dependencies goal 실행
  3. package phase 실행(라이프사이클 순서에 따라 순차적으로)

 

메이븐의 빌드자동화를 정리하면,

  • LifeCycle : 순서를 가지는 Phase의 집합
  • Phase : LifeCycle의 구성요소로 Phase간에는 정해진 순서가 있다.
  • Goal : 메이븐이 수행할 구체적인 작업으로 하나의 페이즈는 하나의 골과 맵핑된다.
  • Plugin : Goal의 집합

라이프사이클에 따른 페이즈 구성이나 설정에 따라 수행되는 골의 종류 등은 메이븐의 공식 사이트를 참고하고 대략적인 개념정리는 여기서 마무리 하도록 한다.


참고

메이븐 웹 - http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference

메이븐파헤치기(김우용) - https://www.slideshare.net/ssuser5445b7/ss-56566336?qid=927855f5-7c8a-4f88-a834-d31292324fd2&v=&b=&from_search=4

 

 


 

 

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

패스워드 암호화에 대해서  (0) 2020.12.06
LastModified 헤더를 이용한 파일변경 체크  (0) 2020.12.06
UTC, GMT, Epoch Time  (0) 2019.10.11
Ajax에 관하여  (0) 2018.12.13
META-INF 디렉터리에 대하여  (1) 2018.04.12

목표 

  • Java8부터 추가된 LocalDateTime, ZonedDateTime에 대한 기본이해 및 간단한 활용

날짜와 시간을 나타내는 자바 API는 여러가지가 있었지만 기능의 부족함으로 사용이 권장되지는 않았다.

(Calendar, Date 등)

하지만 자바8부터 등장한 java.time API는 이전 API의 결함을 상당부분 해결했고, 앞으로 제 역할을 할 것이라 기대된다. 

 

Instant Class

Instant 클래스는 타임라인의 한 점을 나타낸다.

Immutable, thread-safe 하며 now()로 생성된 인스턴스를 리턴받을 수 있다.

equals(), compareTo() 처럼 비교를 위한 함수를 제공하므로 인스턴스끼리 시간비교가 가능하다.

 
    @Test
    public void Instant_Duration_기본() throws InterruptedException {
        Instant start = Instant.now();
        System.out.println("현재시각(인스턴스) : " + start); // 현재시각(인스턴스) : 2019-10-11T04:50:51.759Z
 
        someComplicatedJob(); // 시간텀을 두기 위해서 임의의 JOB을 수행
 
        // Instant는 한 점을 나타내므로 비교가 가능하다.
        Instant end = Instant.now();
        System.out.println("Instant 사이의 비교 - equals() : " + start.equals(end));
        System.out.println("Instant 사이의 비교 - compareTo() : " + start.compareTo(end));
 
        // 비교를 위해서 Duration 클래스를 사용할 수 있다.
        // Duration : 두 Instant 사이에 있는 시간의 양, 불변 클래스
        Duration timeElapsed = Duration.between(start, end);
        System.out.println("JOB 수행에 걸린시간(밀리초) : " + timeElapsed.toMillis());
        System.out.println("JOB 수행에 걸린시간(초) : " + timeElapsed.getSeconds());
        System.out.println("JOB 수행시간이 마이너스인지 ? " + timeElapsed.isNegative());
    }
    
    
 
cs

 

Duration Class

시간차에 대한 작업을 위해 제공되는 클래스이다.

@Test
public void Duration_활용(){
    Duration duration1 = Duration.ofHours(1); // 1시간 차이
    Duration duration3 = Duration.ofHours(3); // 3시간 차이
 
    // 두 Duration이 2배 이상 차이가 나는지 체크하는 코드
    boolean overDoubleFaster1 = duration1.multipliedBy(2).minus(duration3).isNegative();
    System.out.println("duration3이 duration1 보다 2배 더 빠른가 ? " + overDoubleFaster1);
 
    boolean overDoubleFaster2 = duration1.toNanos() * 2 < duration3.toNanos();
    System.out.println("duration3이 duration1 보다 2배 더 빠른가 ? " + overDoubleFaster2);
}
cs

 

LocalDateTime

실제 사람이 사용하는 시간을 나타내는 API이다. Local(지역)이 접두어로 붙은 것에서 유추할 수 있듯이 여러 지역의 시간대(Time Zone) 정보는 포함하지 않는다.

KST나 UTC처럼 시간대를 지정하는 것이 정확한 표현이지만, 실제 활용에서 이러한 시간대는 잘 사용되지 않고 오히려 방해가 될 수 있으므로 만일 시간대를 필요로 하지 않는 작업이라면 LocalDateTime 사용을 고려한다.

 

LocalDate, LocalTime처럼 날짜, 시간에 특화된 API도 있지만 이 둘을 합쳐서 표현할 수 있는 LocalDateTime이 주로 사용된다.

@Test
public void LocalDate_LocalTime_LocalDateTime_기본(){
    // 구역(Zone) 구분이 없는 지역날짜로 now()나 of()로 생성한다.
    LocalDate nowDate = LocalDate.now();
    LocalDate myBirthDay = LocalDate.of(1988, Month.MAY, 26); // '월'을 enum으로 줄 수도 있다.
 
    System.out.println("현재날짜 : " + nowDate); // 2019-10-11
    System.out.println("현재날짜 + 7일 : " + nowDate.plusDays(7L)); // 2019-10-18
    System.out.println("현재날짜 + 7일 : " + nowDate.plusWeeks(1L)); // 2019-10-18
    System.out.println("현재날짜 - 7일 : " + nowDate.minus(Period.ofDays(7))); // 2019-10-04
 
    LocalTime nowtime = LocalTime.now();
    System.out.println("현재시각 : " + nowtime); // 15:16:22.909
    System.out.println("현재시각 + 1시간 : " + nowtime.plusHours(1L)); // 16:16:22.909
    System.out.println("현재시각에서 시간만 3으로 변경 : " + nowtime.withHour(3)); // 03:16:22.909
 
 
    LocalDateTime nowDateTime = LocalDateTime.now();
    System.out.println("현재시각 : " + nowDateTime); // 2019-10-11T15:16:22.909
    System.out.println("현재시각 + 1시간 : " + nowDateTime.plusHours(1L)); // 2019-10-11T16:16:22.909
    System.out.println("현재시각에서 시간만 3 : " + nowDateTime.withHour(3)); // 2019-10-11T03:16:22.909
}
cs

 

ZonedDateTime

LocalDateTime에서 시간대(Time Zone) 개념이 추가된 클래스로 기본적인 사용방법은 비슷하다.

다음과 같이 사용가능한 TimeZoneId를 출력해보면 많은 수가 있는것을 알 수 있다.

@Test
public void ZoneId(){
    // 각 시간대는 America/New_York, Europe/Berlin 등 ID가 존재한다.
    Set<String> availableZoneIds = ZoneId.getAvailableZoneIds(); // 이용할 수 있는 모든 시간대를 얻는다.
    for(String str : availableZoneIds){
        System.out.println(str); // Asia/Aden, America/Chicago, Europe/Luxembourg ...
    }
}
cs

많은 ZoneId 중에서도 아마 많이 사용할 시간대는 Asia/Seoul 과 UTC 일 것이다.

해당 타임존을 인자로하여 ZonedDateTime을 생성 후 출력해보면 인자로 준 시간대 정보가 같이 출력된다. 

UTC의 경우 표준이니 별다른 내용이 없지만, Asia/Seoul의 경우는 표준 UTC 06시23분에 9시간이 더해져서 15시23분임을 알 수 있다.

LocalDateTime에 타임존을 추가하거나 ZonedDateTime에 타임존을 제거하면서 상호변환이 가능하다.

@Test
public void ZonedDateTime(){
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
    System.out.println(now); // 2019-10-11T15:23:08.605+09:00[Asia/Seoul]
 
    ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("UTC"));
    System.out.println(now2); // 2019-10-11T06:23:08.605Z[UTC]
 
}
cs
@Test
public void ZonedDateTime_to_LocalDateTime(){
    ZonedDateTime nowUTC = ZonedDateTime.now(ZoneId.of("UTC"));
    System.out.println(nowUTC);
 
    LocalDateTime nowSeoul = nowUTC.withZoneSameInstant(ZoneId.of("Asia/Seoul")).toLocalDateTime();
    System.out.println(nowSeoul);
 
    ZonedDateTime nowSeoulZonedTime = nowSeoul.atZone(ZoneId.of("Asia/Seoul"));
    System.out.println(nowSeoulZonedTime);
}
cs

ZonedDateTime 인스턴스를 문자열(String)로 변경하거나 반대의 경우도 가능하다. 

변환시 DateTimeFormatter를 인자로하여 원하는 패턴으로 변환한다.

@Test
public void formatting(){
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
 
    String nowStr1 = now.format(DateTimeFormatter.ISO_DATE_TIME); // 2019-10-11T15:48:07.039+09:00[Asia/Seoul]
    String nowStr2 = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss z")); // 2019/10/11 15:48:07 KST
    String nowStr3 = now.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)); // 2019년 10월 11일 금요일 오후 3시 48분 07초 KST
    String nowStr4 = now.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.US)); // Friday, October 11, 2019 3:48:07 PM KST
 
    ZonedDateTime now1 = ZonedDateTime.parse(nowStr1);
    ZonedDateTime now2 = ZonedDateTime.parse(nowStr2, DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss z"));
    ZonedDateTime now3 = ZonedDateTime.parse(nowStr3, DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL));
    ZonedDateTime now4 = ZonedDateTime.parse(nowStr4, DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.US));
 
    System.out.println(now1);
    System.out.println(now2);
    System.out.println(now3);
    System.out.println(now4);
}
cs

 

EpochTime -> LocalDateTime 변환

시간을 표현하는 Long값인 EpochTime을 LocalDateTime으로 변환할 수 있다.

@Test
public void Epoch_to_LocalDateTime(){
    // Epoch Time을 LocalDateTime으로 변경한다.
    LocalDateTime now3 = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.of("Asia/Seoul"));
    System.out.println(now3);
 
    LocalDateTime now4 = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.of("UTC"));
    System.out.println(now4);
}
cs

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

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

ExecutorService 사용법  (0) 2020.12.01
Comparator, Comparable 어떻게 사용할까?  (0) 2020.11.29
Collections를 이용한 정렬(sort method) 활용  (0) 2017.05.09
BigDecimal & BigInteger  (0) 2017.04.18
Array 와 ArrayList 사이의 변환  (0) 2017.03.28

목표

  • UTC, GMT, Epoch Time에 대한 이해

UTC, GMT

그리니치 평균시(Greenwich Mean Time, GMT)는 런던을 기점, 웰링턴을 종점으로 하는 협정 세계시의 빠른시간이다.

세계협정시(Coordinated Universal Time, UTC)와 혼용해서 쓰이므로 같다고 생각하면 된다.

(세부적으로는 차이가 있지만 사용하는데 있어서는 같다고 이해하자.)

 

Epoch Time

1970년 01월 01일 자정(UTC/GMT) 이후로 초단위로 지난시간을 나타낸다.

Epoch Time과 같은 의미의 단어로 Unix Epoch, Unix Time, POSIX Time, Unix Timestamp 등이 있다.


보통 프로그래밍시에 시간 측정을 위해서 사용되는 System.currentTimeMillis()가 Epoch Time을 반환하며, 반환값은 밀리세컨드 단위로 반환되므로 1000을 나눠줘야 초 단위가 된다.

long epoch = System.currentTimeMillis()/1000;

참고 사이트 : https://www.epochconverter.com/

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

LastModified 헤더를 이용한 파일변경 체크  (0) 2020.12.06
Maven 기본  (0) 2019.11.12
Ajax에 관하여  (0) 2018.12.13
META-INF 디렉터리에 대하여  (1) 2018.04.12
TCP/IP  (0) 2017.08.01

스프링 도큐먼트 읽고 정리해보기 / Version 5.1.9.RELEASE


3. Validation, Data Binding, and Type Conversion

스프링에서 제공하는 검증, 데이터 바인딩, 타입변환에 대해 알아본다.

 

3.1 Validation by Using Spring's Validator Interface

스프링은 객체에 대한 검증을 위해 추상화한 Validator 인터페이스를 제공한다.
org.springframework.validation.Validator 인터페이스는 다음 두 가지 메서드를 가진다.

  • supports(Class): Validator가 해당 Class를 처리할 수 있는지 boolean을 반환한다.
  • validate(Object, org.springframework.validation.Errors): Object에 대한 검증을 실시하며 결과를 Errors 객체에 담는다.

가장 Low-Level의 객체에 대한 Validator 사용법은 아래를 참고한다. 검증을 위한 helper 클래스로써 ValidationUtils을 사용한다. ValidationUtils에 대한 자세한 사용법은 문서를 참고하며 간단한 검증 및 결과를 Errors객체에 담는 용도로 사용한다.

// 검증대상 클래스
public class Person {
    private String name;
    private int age;
    // the usual getters and setters...
}
// Validator를 구현하는 클래스
public class PersonValidator implements Validator {
    /**
     * This Validator validates *only* Person instances
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}
// 검증 code snippet
TestPerson maruta = new TestPerson("검증대상", 200); // 검증 오류 대상
Errors errors = new BeanPropertyBindingResult(maruta, "testperson"); // Errors 객체 초기화
validator.validate(maruta, errors); // 검증 실행
if(errors.hasErrors()){ // 검증 오류가 있으면
    for(ObjectError error : errors.getAllErrors()){
        System.out.println("error : " + Arrays.toString(error.getCodes()));
    }
}

위의 예시처럼 커스텀 Validator를 만들어서 사용해도 되지만, 공통적으로 사용할 수 있는 검증을 모아서 제공되는 Validator가 있다. 예를들면 문자열의 길이 체크나, 숫자의 최대값/최소값 체크같은 기본적인 검증을 할 대상 클래스의 인스턴스 변수에 애노테이션(@MIN, @MAX, @SIZE 등)으로 마킹한 후 스프링부트 2.0.5 이상에서 자동등록되는 LocalValidatorFactoryBean에 의해 검증하는 것이다.

자세한 내용은 링크를 참고하자.

 

3.2. Resolving Codes to Error Messages

위 검증의 실행결과로 age에 대한 에러코드는 소스코드에서 설정한 'too.darn.old'를 포함하여 추가적으로 자동 설정된다.

too.darn.old.testperson.age, too.darn.old.age, too.darn.old.int, too.darn.old 

이는 MessageCodesResolver(DefaultMessageCodesResolver)가 객체명이나 프로퍼티명, 타입을 이용해 추가해준 것이며, 에러메세지 출력의 편의성을 위해서 동작한다. 개발자가 설정한 에러코드와 자동으로 추가된 에러코드가 프로퍼티 소스로부터 메세지를 가져오기 위해 사용된다.

 

3.3 Bean Manipulation and the BeanWrapper

  • BeanWrapper
    org.springframework.beans 패키지는 JavaBean 표준에 부합(adhere)하다. JavaBean은 인자가 없는 생성자와 naming convention을 따르는 클래스를 말한다.
    org.springframework.beans 패키지에서 가장 중요한 인터페이스 중 하나는 BeanWrapper이다. 이 인터페이스는 직접적으로 어플리케이션 코드에서 호출되지는 않지만 DataBinder와 BeanFactory가 빈에 프로퍼티를 설정할 때 사용된다.

  • PropertyEditor
    어플리케이션에서 대부분의 값을 문자열로 표현한다. 예를 들어, 빈 메타 설정에 대한 XML파일을 작성할 때 int 타입이나 빈의 타입 등을 모두 문자열로 표현한다. 이러한 문자열을 적절한 타입(Object)으로 변경하는 것이 PropertyEditor이다.
    PropertyEditor는 java.beans 패키지에 포함되어 오래전부터 존재했다.
    스프링에서는 여러 종류의 PropertyEditor의 구현체를 제공한다. org.springframework.beans.propertyeditors 패키지 이하에 위치하며, 대부분은 BeanWrapper의 구현체인 BeanWrapperImpl에 등록되어 사용된다. 자세한 리스트는 링크를 참고한다.
    커스터마이징한 PropertyEditor를 구현하여 사용할 수도 있지만 Thread-Safe하지 않으므로 싱글톤 빈으로 등록해서 사용하는 것은 지양해야 한다.(Scope를 Thread로 지정하여 사용하는 것도 가능하다.)

      Custom PropertyEditor구현 - ExoticType 클래스에 String 값을 바인딩한다.

public class ExoticTypeEditor extends PropertyEditorSupport {
    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}

     
      Controller에서 @InitBinder 메서드에 PropertyEditor를 등록하여 사용할 수 있다. 

@InitBinder
public void initBinder(WebDataBinder webDataBinder){
		webDataBinder.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
}

정리하면, 스프링은 문자열 값에서 객체로의 타입변경을 위해서 jdk에 존재하는 PropertyEditor를 구현한 클래스들을 제공하고 있다. 하지만, 스프링 3 이후 소개된 ConverterFormatterPropertyEditor의 단점을 보완하여 사용되고 있다.

 

3.4 Spring Type Conversion

스프링 3부터 소개된 core.convert 패키지는 타입변환 시스템을 지원한다. 이는 이전까지 사용되던 PropertyEditor의 단점을 보완하면서 대체할 수 있다.

PropertyEditor의 단점 : Stateful 하기 때문에 Thread-Safe 하지 않다. String - Object 사이의 변환만 지원한다.

 

  • Converter SPI
    한 객체를 다른 타입의 객체로 변환하기 위해 스프링에서 제공하는 인터페이스이다.
    Converter 생성을 위해서는 Converter<S, T> 인터페이스를 구현한다.
    S 타입의 객체를 인자로 입력받아서 T 타입으로 변환하여 반환하는 메서드 1개만 존재한다.
    PropertyEditor와 다르게 인자나 반환형에 대한 제한이 없고, Thread-Safe하다.(싱글톤 사용이 가능하다.)

package org.springframework.core.convert.converter;

public interface Converter<S, T> {
    T convert(S source);
}

      간단한 Converter들은 스프링이 이미 구현하여 제공하고 있다.

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {
    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

경우에 따라 Converter 인터페이스에서 추가적인 설정이 가능한 인터페이스를 사용할 수 있다. 예를 들면, String 타입에서 Enum 타입의 객체로 변환하는 것처럼 클래스 체계에 대한 변환로직이 필요한 경우 ConverterFactory 인터페이스를 사용할 수 있고, 좀 더 유연한 변환을 원한다면 GenericConverter를 사용을 고려할 수 있다.

 

  • ConversionService API
    ConversionService는 런타임시에 타입변환 로직을 수행할 통합 API를 정의한다.

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

실제 타입변환은 등록된 Converter들에 의해 수행되며, 대부분의 ConversionService 구현체들은 Converter의 등록을 위해 ConverterRegistry 인터페이스를 구현하고있다. 즉, 용도에 맞춰 수동으로 컨버터를 선택하여 실행하지 않고 ConversionService 하나만 호출하여 변환한다.

ConversionService는 stateless하므로 초기화 된 후 Multi-Thread에서 공유가 가능하다. (PropertyEditor의 단점을 보완)

디폴트 ConversionService를 사용하기 위해서는 빈으로 등록하여 사용하고, 만일 추가적으로 커스터마이징한 컨버터를 등록하고 싶다면 property에 추가하여 사용한다. 타입 변환을 위해 자동으로 사용되지만 소스코드에서 주입받아 사용도 가능하다.

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>

 

3.5 Spring Field Formatting

  • Formatter SPI
    Converter와 같이 타입변환을 위해 제공되는 인터페이스이다. 차이점은 문자열 타입변환만 제공한다는 점이다. Formatter는 Printer와 Parser 두 개의 인터페이스를 상속받고 있다. Printer와 Parser의 메서드는 인자로 Locale을 받아 국제화를 지원할 수 있다.
    메서드의 인자에서 알 수 있듯이 String 문자열을 Locale 정보에 맞게 객체화 시키거나 객체를 Locale 정보에 맞게 String 문자열로 변환하는 기능을 한다.

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}
public interface Printer<T> {
    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;

public interface Parser<T> {
    T parse(String clientValue, Locale locale) throws ParseException;
}

주로 사용되는 용도는 org.springframework.format.number 패키지의 NumberStyleFormatter, CurrencyStyleFormatter 같이 숫자와 관련된 포맷팅이나 org.springframework.format.dateTime 패키지의 DateFormatter 같이 날짜시간에 관련된 포맷팅에 사용된다.

Formatter는 애노테이션에 바인딩되어 편리하게 사용할 수 있다.(Annotation-driven Fomatting)
바인딩하고자 하는 애노테이션으로 AnnotationFormattingFactory를 구현하고, 클래스의 프로퍼티에 애노테이션을 마킹하면 된다.

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);
}
  • FormatterRegistry SPI
    FormatterRegistry 는 Formatter를 등록할 수 있도록 제공되는 인터페이스이면서 Converter를 등록할 수 있는 ConverterRegistry 인터페이스를 확장하고 있으므로 FormatterRegistry 하나로 Converter와 Formatter 등록이 가능하다.
    FormattingConversionService는 FormatterRegistry와 ConversionService 구현 및 확장하고 있는 클래스이다.

    즉 FormattingConversionService 빈 등록을 통해 Formatter와 Converter에 대한 등록과 사용이 가능하다.

    스프링 부트 웹 어플리케이션의 경우에는 DefualtFormattingConversionService을 상속하여 만든 WebConversionService를 빈으로 등록해주며, 이 때 Fomratter와 Converter빈을 찾아 자동으로 WebConversionService에 등록한다.

정리하면, 스프링에서는 타입변환을 위해 타입에 제한이 없는 Converter와 문자열, 국제화에 특화된 Formatter를 제공한다. 직접 호출보다는 자동으로 형변환이 수행될 떄 호출되며 DefualtFormattingConversionService을 통해 등록 및 접근할 수 있다. (하지만, Converter와 Formatter가 빈으로만 등록되어 있다면 자동등록 되므로 수동등록은 하지 않는다.)

 

3.6 Configuring a Global Date and Time Format

@DateTimeFormat을 사용하지 않고 전역적으로 날짜시간 타입의 변환을 위해 DefualtFormattingConversionService 설정할 수 있다.

@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

        // Use the DefaultFormattingConversionService but do not register defaults
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

        // Ensure @NumberFormat is still supported
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

        // Register date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}

3.7 Spring Validation

스프링3 에서부터 검증에 관한 다음 기능이 강화되었다.

1. JSR-303 Bean Validation API 가 지원된다.

  • JSR-303 Bean Validation API

빈 검증에 대해서 애노테이션으로 설정이 가능하다. JSR-303과 JSR-349의 정보가 필요하다면 Bean Validation website를 참고하고, Validator에 대한 정보는 Hibernate Validator를 참고한다.

public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}

 

  • LocalValidatorFactoryBean
    스프링은 javax.validation.ValidatorFactory 와 javax.validation.Validator 를 구현한 LocalValidatorFactoryBean을 제공하므로써 기본 Validator를 빈으로 등록할 수 있게 한다.
<bean id="validator"
    class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

소스코드에서는 다음과 같이 주입받아서 사용하면 된다.

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}

2. DataBinder 가 바인딩 뿐만 아니라 검증도 할 수 있다.

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// bind to the target object
binder.bind(propertyValues);

// validate the target object
binder.validate();

// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

binder.addValidatorsdataBinder.replaceValidators으로 validator를 추가하거나 변경이 가능하다.


3. Spring MVC 에서 @Controller 파라미터에 대한 선언적인 검증이 가능하다.

컨트롤러에서 @InitBinder 메서드에서 Validator를 추가하여 컨트롤러 인풋에 대한 검증이 가능하다.

스프링 도큐먼트 읽고 정리해보기 / Version 5.1.9.RELEASE


2. Resources

2.1 Introduction

리소스에 관한 자바의 표준은 java.net.URL 클래스이고 URL prefix에 따라서 다양한 핸들러들이 존재한다. 하지만 클래스패스로부터 리소스에 접근하는 표준화된 URL 구현체의 부재나 URL 인터페이스가 갖는 기능적인 부족함들이 존재한다.

 

2.2 The Resource Interface

스프링에서는 이러한 단점을 보완하여 Resource 접근에 대해 추상화한 org.springframework.core.io.Resource 인터페이스를 제공한다. (기존의 java.net.URL 을 감싼 형태이다.)

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isOpen();

    URL getURL() throws IOException;

    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();

}

InputStreamSource 인터페이스를 상속받아 리소스의 inputStream을 가져올 수 있는 InputStreamSoure.getInputStream() 메서드를 제공하며, exists() 등 추가 기능을 제공한다.

 

2.3 Built-in Resource Implementations

스프링은 추상화한 Resource 인터페이스의 구현체를 제공하고 있다.

  • UrlResource
    java.net.URL 을 감싼 형태로 URL을 이용하여 리소스에 접근한다. 파일시스템에 접근하기 위한 file: 이나 HTTP 프로토콜을 이용하여 접근하는 http: , FTP 프로토콜을 이용하는 ftp: 같이 접두어를 통해 접근방식을 정한다.
  • ClassPathResource
    classpath 상에 존재하는 리소스에 대한 접근을 위한 리소스 구현체이다. 명시적으로 ClassPathResource 생성자를 이용하여 생성할 수도 있고, 문자열에 classpath: 접두어로 생성될 수도 있다.
  • FileSystemResource
  • ServletContextResource 웹 어플리케이션 루트에서 상대경로로 리소스를 찾는 구현체이다.
  • InputStreamResource
  • ByteArrayResource

 

2.4 The ResourceLoader

ResourceLoader 인터페이스는 Resource 인스턴스를 리턴받기 위한 용도로 사용한다.

public interface ResourceLoader { 
    Resource getResource(String location);
}

ApplicationContext는 ResourceLoader를 구현하고 있으므로 ApplicationContext를 이용하여 리소스를 리턴받을 수 있다.

ApplicationContext extends ResourcePatternResolver
ResourcePatternResolver extends ResourceLoader

ApplicationContext도 인터페이스이기 때문에 어떤 ApplicationContext 구현체에게 getResource()를 호출하느냐에 따라 리턴되는 Resource의 구현체가 달라진다. 예를 들어, ClassPathXmlApplicationContext에서 getResource()를 호출한다면 ClassPathResource가 리턴된다. 이러한 식으로 ApplicationContext와 리턴되는 Resource의 타입은 맵핑된다.

 

FileSystemXmlApplicationContext- FileSystemResource

WebApplicationContext - ServletContextResource

 

만일 ApplicationContext 구현체의 타입에 상관없이 특정 타입의 Resource를 리턴받고 싶다면 getResource()에 대한 문자열 인자에 Prefix를 부여하면 된다.

// ClassPathResource 리턴
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");

// FileSystemResource 리턴
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");

// UrlResource 리턴
Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");

정리하자면, ResourceLoader로부터 리턴받는 Resource의 타입은 ApplicationContext의 구현체의 타입에 따라 다르며, 특정 타입의 Resource를 리턴받고자 할 때는 문자열 인자에 알맞은 prefix를 부여하면 된다.

 

2.5 The ResourceLoaderAware interface

ResourceLoaderAware는 콜백 인터페이스로 컨텍스트에 생성된 ResourceLoader 빈을 가져올 수 있다.

public interface ResourceLoaderAware {
    void setResourceLoader(ResourceLoader resourceLoader);
}

물론 ApplicationContext가 ResourceLoader를 구현하고 있기 때문에 ApplicationContextAware로 컨텍스트를 이용하여 리소스에 접근할 수 있지만, 추천되지는 않는다. 해당 빈을 가져오고자 하는 이유가 리소스의 접근이라면 그 용도로 설계된 ResourceLoader 타입의 빈을 가져오는 것이 맞다.

2.6 Resources as Dependencies

만일 특정 빈이 고정된(static) 리소스를 필요로 한다면 프로퍼티로써 ResourceLoader를 사용하지 않고 Resource 빈을 주입할 수 있다. 예시처럼 value속성을 문자열로 주지만 Resource로 빈 주입이 가능한 것은 ApplicationContext가 등록하고 사용하는 특별한 JavaBean인 PropertyEditor가 있기 때문이다.

<bean id="myBean" class="...">
    <property name="template1" value="some/resource/path/myTemplate.txt"/>
    <property name="template2" value="classpath:some/resource/path/myTemplate.txt">
    <property name="template3" value="file:///some/resource/path/myTemplate.txt"/>
</bean>

 

2.7 Application Contexts and Resource Paths

XML을 베이스로 한 ApplicationContext를 생성시 Resource를 활용하는 방법에 대해 알아본다.

  • ApplicationContext 생성시 Resource 인자

    ApplicationContext을 소스코드로 생성자를 이용하여 생성할 때 인자로 문자열(String)을 지정한다. 문자열이 해석되어 특정 타입의 Resource객체로 XML파일이 로드된다. 문자열에 prefix를 지정하지 않으면 ApplicationContext의 구현체 타입에 따라 Resource의 타입이 결정되어 로드한다.

// ClassPathResource로 빈 설정파일이 로드된다.
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

// FileSystemResource로 빈 설정파일이 로드된다.
ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");

 

  • ApplicationContext 생성시 와일드카드(*) 사용

    와일드카드를 이용하여 Ant-Style Patterns을 활용할 수 있다.

/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml

      또는 classpath*: prefix를 활용할 수 있다.

// classpath에서 해당 이름의 모든 xml이 로드된다.
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

      Ant-Style과 classpath*: prefix를 혼용하여 사용할 수도 있다.

 

  • FileSystemResource를 사용할 때 주의점 FileSystemApplicationContext는 모든 FileSystemResource 인스턴스들에 대해서 슬래쉬(/)의 유무에 상관없이 상대경로를 사용하도록 강제한다. 즉 아래 예시는 동일하게 상대경로를 지정한다.

// 1번
ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/context.xml");

// 2번
ApplicationContext ctx = new FileSystemXmlApplicationContext("/conf/context.xml");

      그러므로 만일 절대경로를 사용하고 싶으면 file:/// prefix를 활용하여 UrlResource를 사용하도록 한다.

// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx = new FileSystemXmlApplicationContext("file:///conf/context.xml");

// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");

스프링 도큐먼트 읽고 정리해보기 / Version 5.1.9.RELEASE


1.9 Annotation-based Container Configuration

빈 설정 메타데이터를 XML이나 애노테이션을 이용하여 작성할 수 있지만, 어떤 것이 절대적으로 좋다고 말할 수는 없다.(it depends) 그리고 XML방식과 애노테이션 방식을 혼용하여 사용할 수도 있으나 한쪽 설정이 오버라이드 될 수 있는 점을 고려해야 한다.

애노테이션에 기반한 설정 방식은 앞서 봤던 BeanPostProcessor를 이용하여 ApplicationContext가 빈 조립을 가능하게 해 준다. 빈으로 등록되는 클래스에 애노테이션을 마킹하고 BeanPostProcessor에서 애노테이션을 검사하여 특정 로직을 실행하는 방식이다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

</beans>

위 XML에서 <context:annotation-config/>를 사용하면 아래 BeanPostProcessor가 자동으로 등록되고, 빈 생성 및 초기화시 동작한다.

 

AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor, RequiredAnnotationBeanPostProcessor


애노테이션 빈 설정에서 사용되는 애노테이션

  • @Autowired
    클래스의 생성자, 세터 메서드, 필드 등에 사용하여 컨테이너에서 의존성을 주입받는다.
    타입 기준으로 빈이 주입되며, 해당 타입의 빈이 여러 개인 경우 빈 이름에 따라 주입된다.
    같은 타입의 빈이 여러개 일 때 특정 빈을 주입하고자 한다면 아래 3가지를 고려한다.
    1. @Primary
    2. 배열, 컬렉션, 맵으로 모든 빈을 주입받기 : @Autowired private List<Movie> movies;
    3. @Qualifier
  • @Primary
    빈 주입을 위한 후보자 빈이 여러 개일 때 특정 빈을 @Primary로 마킹하면 해당 빈이 주입된다.
  • @Qualifier
    @Primary 와 같이 빈 주입 시 후보군 중에서 특정 빈을 선택하고자 할 때 사용한다.
    @Qualifier("name") 형태로 특정 빈에 마킹을 하고, 주입받을 빈에서도 @Autowired와 함께 @Qualifier("name") 를 마킹한다.
    기존의 애노테이션을 확장하여 커스터마이징한 @Qualifier를 만들 수 있으며, 내용은 링크를 참고한다.
  • @Resource
    JSR-250에서 정의된 @Resource 애노테이션을 의존성 주입에 사용할 수 있다.
    @Autowired와 다른 점은 name 속성으로 빈을 주입한다는 점이며 일치하는 빈 이름이 없는 경우에는 타입을 이용하여 빈을 주입한다.
    name속성을 제거하여 사용하는 경우 필드명이나 메서드의 파라미터명과 일치하는 빈 이름을 찾는다. @Resource(name="myMovieFinder")
  • @PostConstruct, @PreDestrory
    메서드에 마킹하면 빈 라이프사이클에 따라 호출된다.

1.10 Classpath Scanning and Managed Components

XML을 이용한 빈 설정 메타데이터를 사용하지 않고 classpath를 스캐닝하여 빈을 등록하는 방법에 대해 알아본다. @Component 와 같은 애노테이션이나 AspectJ 타입 표현 등을 사용할 수 있다.

  • @Component 및 이를 확장한 스테레오타입 애노테이션
    스프링에서는 스캐닝을 통한 빈 등록을 위해 @Component를 포함한 여러 애노테이션을 제공한다. 클래스에 애노테이션이 마킹되어 있으면 빈으로 등록하는 식이다.
    빈의 기능이 명확하게 정의되어 있는 경우라면 @Component를 확장한 스테레오타입 애노테이션을 사용하는 것이 추천된다.
    빈의 기능(또는 계층)이 명확하게 정의되었다는 것은 일반적으로 말하는 DAO(Data Access Object) 객체와 같은 것을 의미한다. DAO의 기능을 하는 객체는 @Repository라는 @Component를 확장한 애노테이션으로 마킹하여 빈으로 등록한다. 이 외에도 @Controller, @Service 등의 스테레오타입 애노테이션 마킹을 활용하면 로깅, 예외처리, AOP 적용에 용이하다.

  • Meta-Annotation, Composed-Annotation
    다른 애노테이션에 마킹되어 적용되는 애노테이션을 Meta-Annoation이라 하며, 메타 애노테이션의 조합으로 만들어진 애노테이션을 Composed-Annotation이라 한다.
    Meta-Annotation 예시 : @Service 애노테이션에 마킹된 @Component
    Composed-Annotation 예시 : @ResponseBody와 @Controller의 조합으로 이루어진 @RestController
    더 자세한 설명은 링크를 참조한다.

  • Bean Scanning
    클래스에 애노테이션을 마킹했다면 빈으로 ApplicationContext에 등록하기 위해서 특정 패키지를 지정하여 빈 스캐닝을 진행한다. 빈 스캐닝 과정에서 클래스로 작성한 빈 설정 메타데이터나 @Service, @Controller 등으로 마킹된 클래스들이 빈으로 등록된다.
    자바 빈 설정 파일에서는 @ComponentScan(basePackages="org.expample") 으로 지정하고,
    XML설정에서는 <context:component-scan base-package="org.example"/> 으로 지정한다.
    <context:component-scan />이 위에 언급한 <context:annotation-config /> 를 포함하므로 중복으로 설정할 필요가 없다.

  • ComponentScan - Filter
    컴포넌트 스캔 시 base-package를 기준으로 @Component와 이를 확장한 애노테이션들이 모두 빈으로 등록된다. 이러한 디폴트 스캔 방식을 커스터마이징 하고 싶다면 Filter를 이용한다.
    Filter는 특정 클래스나 애노테이션 등을 제외하거나 포함시킬 수 있다.

@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    ...
}
  • 스테레오 타입이 마킹된 클래스에서 생성되는 빈들은 BeanNameGenerator에서 기본 전략으로 생성한 이름을 갖는다. @Service("myBeanName")처럼 빈 이름을 지정하지 않으면 클래스명의 첫글자를 소문자로 바꿔서 디폴트 빈이름을 설정한다.

  • @Scope("prototype") 애노테이션으로 특정 빈의 스코프를 디폴트(싱글톤)와 다르게 지정할 수 있다.

1.11 Using JSR-330 Standard Annoatations

JSR-330에서 등장한 애노테이션들을 사용할 수 있도록 스프링에서 지원하고 있다. 지원하는 기능을 사용하기 위해서는 javax.inject 라이브러리의 추가가 필요하다.

스프링에서 제공하는 애노테이션과 유사하게 사용될 수 있는 JSR-330 애노테이션들이 존재한다.
@Autowired - @Inject, @Component - @Names / @ManagedBean 등

하지만 완벽하게 대응하지는 않고 스프링 애노테이션과 비교하면 제공하는 기능에서 한계점이 있으니 고려하여 사용해야 한다. 자세한 비교는 링크를 참조한다.

1.12 Java-based Container Configuration

자바 코드를 통한 컨테이너 구성에 대해 알아본다. 자바 코드로 빈 설정 메타데이터를 만든다는 의미이다.

  • @Bean / @Configuration 기본

    스프링의 자바 코드 설정에서 가장 중심이 되는 애노테이션이다. 보통 @Configuration이 마킹된 클래스 파일이 빈 설정 파일이 되고, @Configuration 클래스 안에 @Bean이 마킹된 메서드의 반환 객체를 컨테이너에서 관리하는 빈으로 등록한다.

@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl(); // 반환되는 MyService 객체가 빈으로 등록된다.
    }
}
  • AnnotationConfigApplicationContext

    자바 코드를 이용한 빈 설정에서 사용되는 ApplicationContext를 확장한 클래스이다.
    코드로 생성 시 빈설정 클래스를 생성자에 인자로 하여 생성할 수도 있고, 아래 예시처럼 생성할 수도 있다.

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh(); // 설정 클래스의 빈들이 초기화되고 등록된다.
    
    // ApplicationContext 에서 등록된 빈을 가져올 수 있다.
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

일반적으로 웹 어플리케이션에서는 AnnotationConfigWebApplicationContext이 사용되며, 이것도 ApplicationContext로부터 확장된 클래스이다.

  • @Bean 사용

    자바 설정 파일의 메서드에 @Bean을 마킹하여 리턴값을 빈으로 등록한다.

    빈으로 등록되는 객체에 대해 라이프사이클 콜백 메서드를 지정할 수 있다. JSR-250에 정의된 @PostConstruct, @PreDestory를 메서드에 마킹하거나, 스프링에서 제공하는 InitializingBean, DisposableBean, Lifecyle 등의 인터페이스의 메서드를 구현하거나, *Aware 인터페이스의 메서드를 구현하면 BeanPostProcessor에서 해당 메서드를 실행해준다.


    *Aware 인터페이스 예시 : BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware


    @Bean 애노테이션을 마킹할 때 인자로 추가적인 설정을 할 수 있다.
    @Bean(initMethod = "init"), @Bean(destroyMethod = "cleanup") : 라이프사이클 메서드 지정 @Scope("prototype") : 빈 스코프 지정
    @Bean(name = "myThing") : 빈 이름 지정
    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"}) : 이름은 복수개를 동시에 줄 수 있다.
    @Description("Provides a basic example of a bean") : 빈 설명 지정
  • @Configuration 사용

    자바 클래스파일에 빈설정 파일임을 나타내는 @Configuration 마킹을 할 수 있으며, 아래 애노테이션과 사용된다.
    @Import : 다른 빈 설정 클래스 파일을 포함한다.

    @ImportResource : 다른 XML 빈 설정 파일을 포함한다.

    XML 중심의 빈 설정에서는 <bean /> 태그를 이용하여 클래스 설정 파일을 서브로 포함시킬 수 있고, 클래스 중심의 빈 설정에서도 @ImportResource를 이용하여 XML 설정 파일을 서브로 포함시킬 수 있다.

1.13 Environment Abstraction

Environment는 스프링에서 제공하는 인터페이스로 어플리케이션에서 profiles와 properties에 대한 관리를 위해 사용한다.

  • @Profile

    Profile 은 환경에 따른 빈들의 그룹이다. 어플리케이션의 사용자(개발자 또는 QA)나 운영환경(개발 or 운영 or 테스트)에 따라 사용하고자 하는 빈이 다를 때 빈을 그룹핑하여 환경에 맞게 컨테이너에 등록하게 해 준다.

    @Configuration 클래스나 @Bean 메서드에 @Profile("name")을 마킹하여 Profile을 설정한다.
    어플리케이션 구동 시 아무런 설정을 해주지 않으면 디폴트로 @Profile이 설정되지 않은 빈들이 등록되고, @Profile("name")이 설정된 빈은 등록되지 않는다.
    @Profile("name")이 설정된 빈들만 등록하고 싶다면 아래와 같이 Profile을 지정해준다.

    1. Environment에 Profile을 설정

      AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setActiveProfiles("development"); ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); ctx.refresh();
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

        2. JVM 옵션에 Profile 을 설정 : -Dspring.profiles.active="profile1,profile2"

Profile은 표현식으로 지정할 수도 있다.

        XML 설정에서도 <bean /> 태그 안에서 프로퍼티로 Profile 설정이 가능하다.

  • PropertySource

    Environment는 등록된 Property에 대한 접근을 가능하게 해 준다. Property가 등록된 저장소를 PropertySource라 한다.

    스프링의 StandardEnvironment에서는 두 개의 PropertySource객체가 있다.

    1. JVM system properties : System.getProperties()
    2. system environment variables : System.getenv()

    일반적인 웹 어플리케이션의 StandardServletEnvironment에는 5개의 PropertySource 가 있고 이들 사이에는 다음과 같은 우선순위를 가진다. 같은 프로퍼티명을 가지는 프로퍼티를 여러곳에 선언했다면 아래 우선순위에 따라 오버라이드 된다.

    1. ServletConfig parameters (if applicable — for example, in case of a DispatcherServlet context)
    2. ServletContext parameters (web.xml context-param entries)
    3. JNDI environment variables (java:comp/env/ entries)
    4. JVM system properties (-D command-line arguments)
    5. JVM system environment (operating system environment variables)

    기본으로 추가되는 PropertySource 외에도 개발자가 생성한 PropertySource를 추가할 수 있다.
    아래 예시는 classpath에 app.properties 파일을 생성하고 testbean.name 프로퍼티의 값을 소스에서 주입받는 예시이다. @PropertySource 애노테이션으로 PropertySource를 생성한다.

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Value("${testbean.name}") // @Value 애노테이션으로 프로퍼티 주입
    String testName;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        
        // Environment 객체를 이용하여 프로퍼티 조회
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

      ${...} placeholder 표현식 안에서 ${...} 표현식이 사용될 수 있다.
      @PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")

1.14 Registering a LoadTimeWeaver

스프링은 클래스가 JVM에 로드될 때 동적으로 변경하기 위해 LoadTimeWeaver를 사용한다.
@Configuration 클래스에 @EnableLoadTimeWeaving 마킹을 추가하여 설정하면, ApplicationContext의 빈들이 LoadTimeWeaverAware을 구현하여 load-time weaver instance에 대한 참조가 가능하다.

JPA class transformation에 유용하게 사용된다.

1.15 Additional Capabilities of the ApplicationContext

org.springframework.beans.factory 패키지는 프로그램적으로(programmatic) 빈을 관리하고 조작하기 위한 기본 기능들을 제공한다. ApplicationContext를 포함한 org.springframework.context 패키지의 클래스는 factory 패키지를 확장하여 좀 더 프레임워크적(framework-oriented style)으로 기능들을 제공한다. 일반적인 어플리케이션에서 ApplicationContext를 소스코드로 생성하지 않고 지원되는 ContextLoader를 이용하여 자동으로 객체화하는 것이 그 예이다.

프로그램적(programmatic) : 라이브러리는 제공해주지만 개발자가 소스코드로 생성하고 조기화하여 사용해야 한다.

프래임워크적(framework-oriented style) : 설정을 통해 자동으로 생성되고 초기화되고 실행된다.

BeanFactory : 프로그램적으로 생성하고 빈을 조작한다.
ApplicationContext : 프레임워크적으로 생성하고 빈을 조작한다.


BeanFactory에서 기능적으로 확장된 ApplicationContext의 추가 기능에 대해 알아본다.

  • MessageSource : 국제화(i18n) 기능을 제공

    ApplicationContext는 MessageSource 인터페이스를 구현하여 i18n 이라 부르는 국제화를 지원한다. MessageSource 인터페이스를 확장한 HierarchicalMessageSource 인터페이스를 제공하여 메세지가 계층형으로 해석될 수 있도록 한다. (메세지 키(코드)가 중복되는 경우 오버라이드 된다.)

    ApplicationContext에 messageSource 이름을 갖는 빈이 선언되어 있어야 한다. ApplicationContext는 해당 이름의 빈이 없으면 자신의 상위(parent)에서 빈을 찾는다. 만일 연결된 상위 context에서 빈을 찾을 수 없으면 비어있는 DelegatingMessageSource가 요청을 받기위해 초기화된다.


    스프링에서는 MessageSource에 대한 구현체로 ResourceBundleMessageSource, StaticMessageSource 두 가지를 제공하지만 주로 ResourceBundleMessageSource이 사용된다. (스프링부트에서도 resources 이하 디렉터리에 message.properties 파일로 ResourceBundleMessageSource이 생성된다.)

    프로퍼티 파일과 ApplicationContext에서 MessageSource를 사용하는 예시

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>
# in format.properties
message=Alligators rock!
public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", null);
    System.out.println(message);
}

      메세지를 로딩하는 과정에서 국제화(i18n)을 적용하고자 한다면 properties 파일의 파일명을 규칙에 맞게 작성한다.

      만일 영국(en-GB) 지역의 국제화를 제공하고 싶다면 파일명을 XXX_en_GB.properties로 메세지 프로퍼티 파일을
      생성하고, getMessage() 호출시 Locale 인자를 Locale.UK 로 한다.

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the {0} argument is required, I say, required.
public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

      ResourceBundleMessageSource 가 아닌 리로딩 기능이 있는 MessageSource를 사용할 수도 있다.

@Bean 
public MessageSource messageSource() { 
	var messageSource = new ReloadableResourceBundleMessageSource();
	messageSource.setBasename("classpath:/messages");
	messageSource.setDefaultEncoding("UTF-8");
	messageSource.setCacheSeconds(3); 
	return messageSource; 
}
  • Standard Event / Custom Event

    ApplicationContext는 이벤트를 핸들링 하는 기능을 다음 두 인터페이스를 통해 제공하고 있다.

    ApplicationEvent, ApplicationListener

    ApplicationContext는 뒤에 나올 ApplicationEventPublisher를 구현하고 있다. Observer design pattern을 사용하여 ApplicationEvent가 ApplicationContext로 발행(publish)되면 ApplicationListener빈으로 이벤트가 전달된다.

    스프링에서는 다음의 Built-in Event를 제공하고 있다. 자세한 설명은 문서를 참고하자.

    ContextRefreshedEvent, ContextStartedEvent, ContextStoppedEvent, ContextClosedEvent, RequestHandledEvent


    커스텀 이벤트 발행/수신 예시

    1. ApplicationEvent를 구현한 Event 클래스 작성
    2. ApplicationEventPublisherAware 를 구현하여 publisher에 작성한 이벤트를 발행
      ApplicationEventPublisher의 publishEvent() 메서드를 호출한다.
    3. ApplicationListener에서 발행된 이벤트를 수신하여 처리
      리스너는 ApplicationListener를 구현하거나 메서드에 @EventListener를 마킹한다. @EventListener는 스프링 4.2부터 지원하는 기능이다.

    이벤트를 발행하고 리스너에서 처리하는 과정은 동기화(synchronously) 된다. 즉, 하나의 트랜잭션으로 묶어 처리가 가능하다.
    이벤트 처리의 순서를 지정하고 싶다면 @Order 애노테이션, 비동기로 처리되기를 원한다면 @Async 애노테이션과 함께 사용한다.

  • Low-level Resources에 대한 편리한 접근

    ApplicationContext은 ResourceLoader로써 Resources에 대한 편리한 접근이 가능하다. 구체적인 것은 2장 Resources에서 알아본다.

  • 웹어플리케이션에서의 편리한 ApplicationContext 초기화

    ApplicationContext를 소스코드로 생성/초기화 하여 사용해도 되지만 웹어플리케이션에서는 ContextLoaderListener를 사용해 편리하게 생성하고 초기화 할 수 있다.

    web.xml 에 다음과 같이 작성하면 ContextLoaderListener가 어플리케이션이 구동될 때 contextConfigLocation 인자를 검사하여 ApplicationContext를 초기화한다. 만일 해당 인자가 없다면 디폴트 파일(/WEB-INF/applicationContext.xml)이 빈 설정을 위한 메타데이터로 이용된다.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
  • ApplicationContext를 Java EE RAR File 로 배포

    일반적인 WAR 파일 배포대신에 RAR 파일로 배포가 가능하다는 것 같다. 많이 사용되는 방식은 아닌것 같으므로 생략한다.

1.16 The BeanFactory

BeanFactory(DefaultListableBeanFactory)는 기본적인 Spring의 IoC 기능을 API 제공하고 있다. 그럼에도 ApplicationContext 가 주로 사용되는 이유는 BeanFactory의 기능을 포함한 강력한 추가기능에 있다. 예를 들면, BeanPostProcessor를 이용한 기능확장 등이다.

ApplicationContext에서는 미리 정해진 기본 규약에 의해 여러 종류의 빈들이 등록(detected)된다.
(빈 이름 또는 타입에 의해 - 특히 post-processor)


반면에 DefaultListableBeanFactory는 이런 특별한 빈들을 알지 못하므로 등록을 위해서는 다음과 같이 소스코드로 등록을 해주어야 한다.

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions

// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory


BeanFactory VS ApplicationContext

스프링 도큐먼트 읽고 정리해보기 / Version 5.1.9.RELEASE


1. The IoC Container

1.1 Introduction to the Spring IoC Container and Beans

IoC와 DI는 유사한 개념으로 객체(빈) 사이의 의존성을 별도의 IoC 컨테이너에서 관리한다.

IoC 컨테이너에 의해서 객체가 생성되고, 초기화되고, 조립되며 이렇게 IoC 컨테이너에 의해서 관리되는 객체를 Bean이라 부른다.

IoC컨테이너는 Bean 관리를 위해서 설정정보(configuration metadata)를 사용한다. 설정정보는 XML파일이나 자바 애노테이션 또는 자바 코드가 될 수 있다.

 

1.2 Container Overview

스프링에서 IoC컨테이너는 BeanFactory 인터페이스를 말하며, 실제 어플리케이션에서는 이를 확장한 ApplicationContext를 의미하는 것이 일반적이다.

비즈니스가 포함된 자바 소스(POJOs)와 빈 설정관련 정보가 있으면 스프링컨테이너에 이를 읽어들여 실행가능한 어플리케이션을 구성해준다.

ApplicationContext는 인터페이스이므로 ApplicationContext를 생성(초기화)하고자 한다면 설정파일(빈 설정에 사용되는 메타데이터)의 타입에 따라 클래스를 선택한다. 스프링에는 이미 구현된 다수의 클래스들을 제공하고 있다.

 

제공되는 구현 클래스 예시 : ClassPathXmlApplicationContext(XML), AnnotationConfigApplicationContext(JAVA)

 

ApplicationContext가 제공하는 T getBean(String name, Class<T> requiredType) 메서드를 통해 IoC컨테이너에서 관리되는 빈을 가져올 수 있지만 어플리케이션 소스에서 직접 빈을 가져오는 Spring API를 사용하는 것은 안된다.

@Autowired 애노테이션처럼 스프링에서 DI를 위해 제공하는 방법에 따라 빈을 DI받아야 한다.

 

1.3 Bean Overview

빈 설정파일에는 빈 정의에 대한 내용이 담겨있으며 상황에 따라 클래스파일이나 XML파일을 선택하여 작성한다.

빈 정의시 Class, Name, Scope 등 여러 Property들을 정의할 수 있다.

 

1.4 Dependencies

어플리케이션은 한 개의 객체로만 구성되어 있지 않고 여러 객체가 서로 상호작용하며 동작한다. 한 객체가 다른 객체를 사용할 때 의존성(Dependency)을 가진다고 한다.

 

스프링에서는 객체 간의 의존성을 스프링 컨테이너가 관리하며, 객체가 생성되고 초기화 될 때 객체간의 의존성이 구성된다. 즉, 소스코드 자체에서 의존성을 관리하는 것이 아니라 컨테이너가 메타데이터를 이용해 객체간의 의존성을 구성해주므로 의존성 주입(Dependency Injection)이라 한다.

 

컨테이너에 의한 의존성 주입은 객체의 생성자나 세터(setter) 메서드를 이용하여 실행된다.

 

1.5 Bean Scopes

스프링 컨테이너가 생성하는 빈들의 디폴트 스코프는 싱글톤이며, 이를 포함하여 지원하는 스코프는 총 6개가 있다. 빈설정 메타데이터에 스코프를 지정 가능하며 대부분의 경우에는 디폴트인 싱글톤을 사용한다.

 

Singleton, Prototype, Request, Session, Application, Websocket

 

빈이 stateless 할 때는 싱글톤을 사용해도 괜찮지만 빈이 stateful 하다면 프로토타입을 고려한다. 단 싱글톤 빈이 프로토타입 빈을 참조할 때는 항상 같은 빈이 참조되는 상황을 생각해야 한다.

 

(참고) 싱글톤 빈이 프로토타입 빈을 참조하면서 업데이트가 되게 하는 3가지 방법

  1. scoped-proxy 2. Object-Provider 3. Provider(표준)

1.6 Customizing the Nature of a Bean

스프링 컨테이너에서 빈이 생성되고 초기화되는 과정 외에도 빈을 커스터마이징 할 수 있다.

  • Lifecycle Callbacks
    라이프사이클에 따라서 호출될 콜백메서드를 지정한다.
    InitializingBean 인터페이스의 afterPropertiesSet()과 DisposableBean 인터페이스의 destroy() 를 구현함으로써 빈 생성 이후 동작을 지정할 수 있지만 스프링의 라이브러리를 코드에서 직접적으로 사용한다는 점에서 추천되지 않는다.
    대신 JSR-250의 @PostConstruct 와 @PreDestroy 를 활용하면, 스프링 소스에 의존하지 않고 커스터마이징을 할 수 있다.(XML에서는 init-method 와 destroy-method 속성을 활용한다.)

@Component
public class TestPerson implements InitializingBean, DisposableBean {
    private String name;
    private int age;

    @PostConstruct
    public void init(){
        // 빈 생성 후의 동작을 지정한다.
    }

    @PreDestroy
    public void end(){
        // 빈 종료 후의 동작을 지정한다.
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 빈 생성 후의 동작을 지정한다.
    }

    @Override
    public void destroy() throws Exception {
        // 빈 종료 후의 동작을 지정한다.
    }
}
  • ApplicationContextAware / BeanNameAware 위 인터페이스의 메서드를 구현하여 ApplicationContext와 BeanName을 확인할 수 있다. ApplicationContext를 이용하여 소스코드에서 빈을 커스터마이징할 수 있다. 하지만 이 방법도 스프링의 라이브러리를 코드에서 직접 사용하는 것이므로 추천되지 않는다.

public interface ApplicationContextAware { 
	void setApplicationContext(ApplicationContext applicationContext) throws BeansException; 
}
  • 그 외 Aware 인터페이스 ApplicationContextAware 와 BeanNameAware 을 포함하여 스프링에서 제공하는 Aware 인터페이스들이 있다. 자세한 내용은 링크 참조. 하지만 이런 Aware들을 사용하는 것도 스프링 라이브러리에 의존하며, IoC 스타일에 맞지 않으므로 지양하자.

1.7 Bean Definition Inheritance

XML을 이용한 빈 설정시 parent 속성을 이용하여 다른 빈의 설정을 그대로 적용할 수 있다. 소스코드를 이용한 메타데이터 설정시에는 그다지 필요한 부분은 아닌듯 하다.

 

1.8 Container Extension Points

개발자가 ApplicationContext 의 구현체를 직접 사용하지 않고 제공되는 통합 인터페이스(integration interface)를 이용하여 컨테이너를 확장 할 수 있다.

  • BeanPostProcessor 인터페이스로 빈을 커스터마이징
    BeanPostProcessor는 ApplicationContext가 빈을 초기화하는 과정에 동작한다. 그러므로 해당 인터페이스를 구현한 BeanPostProcessor 객체(빈)를 컨테이너에 등록하면 빈 초기화 과정에서 추가적인 동작(초기화 로직, 의존성 변경 등)을 지정할 수 있다. 다수의 BeanPostProcessor 가 동작하는 경우 Ordered 인터페이스를 구현하여 동작 순서를 지정한다.
    빈 개별마다 초기화 동작을 지정하고 싶다면 @PostConstruct 를 사용하고 공통적으로 지정할 로직은 BeanPostProcessor 의 사용을 고려한다.

  • BeanFactoryPostProcessor 인터페이스로 메타데이터를 커스터마이징
    BeanPostProcessor 와 갖는 의미(Semantic)은 유사하지만 큰 차이점은 빈 설정 메타데이터에 대한 조작이 가능하다는 점이다. 스프링 컨테이너는 빈 초기화 전에 등록된 BeanFactoryPostProcessor 구현체에게 메타데이터에 대한 변경을 가능하게 해준다.
    여러개의 BeanFactoryPostProcessor 가 존재할 경우 Ordered 인터페이스를 구현하여 동작 순서를 지정한다.
    예시로는 PropertyPlaceholderConfigurer 가 있는데, 빈 설정 메타데이터에서 ${property.name} 와 같이 placeholder 안에 있는 값을 지정된 외부위치로부터 가져와 치환해준다.

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close"
        class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

PropertyPlaceholderConfigurer(BeanFactoryPostProcessor의 구현체)를 빈으로 등록하면, 빈 초기화시 동작하여 placeholder로 표현한 프로퍼티 값이 주입된다.

  • FactoryBean 인터페이스로 빈 초기화를 커스터마이징
    빈 설정 메타데이터를 XML방식으로 사용하면서, 복잡한 빈 초기화 로직을 작성하고자 한다면 자바소스로 작성하는 것이 더 낫다. 이럴 때 FactoryBean 을 작성하여 <bean />의 class 프로퍼티에 지정해주면 FactoryBean.getObject() 가 리턴하는 객체가 빈으로 등록된다.
    ApplicationContext 객체로부터 FactoryBean 의 실제 구현체를 가져오고자 하는경우 getBean("&name") 으로 '&' 을 prefix로 하여 호출하면 된다.(getBean("name")은 FactoryBean의 구현체가 리턴된다.)

예전 프로젝트를 했었을 때도 그렇고 지금 하고있는 웹 어플리케이션에서도 마찬가지로 Ajax를 쓰고있다. 

그저 편하게 남들 소스를 가져와서 수정만 했을뿐이지 어떻게 돌아가는지 찾아보거나 하지는 않았다.

그래서 글을 남긴다.


일반적으로 form submit()을 통해 서버에 요청을 보내면 결과값으로 HTML이 브라우져로 전달되면서 화면이 전체적으로 다시 그려지게 된다. 하지만 수행하고자 하는 작업에 따라서 원하는 영역에만 결과값을 보여주고 싶거나, 서버에서 응답이 오기전에 다른 작업을 수행해야 할 수도 있다. 그래서 사용하는 것이 Ajax이다.


[위키에서 가져온 Ajax를 사용했을 때와 아닐때의 비교]

왼쪽과 같이 Ajax를 사용하지 않을경우 화면에 표시할 HTML과 각종 CSS데이터를 모두 서버에서 받아오지만, Ajax를 사용하면 원하는 결과값만 XML 또는 JSON과 같은 형태로 받아올 수 있다.


이를 가능해게 해주는 핵심 구성요소가 바로 XMLHttpRequest(xhr)이다. Ajax는 이놈의 객체를 이용해서 서버와 비동기로 통신을 할 수 있다. 익스플로러 5버전에서는 ActiveXObject 라는 객체를 이용해서 비동기식 통신을 구현했다고는 하나, 요즘 나오는 익스플로러 버전이나 파이어폭스, 크롬 등 대부분의 브라우저에서는 XHR을 사용한다고 하니 무시해도 될 것 같다.


브라우저 콘솔에서 쉽게 var object1 = new XMLHttpRequest(); 로 객체를 생성할 수 있다. 이 객체에 내장된 open(), send() 메서드를 사용하여 서버와 비동기식 통신을 하고, readyState 프로퍼티, status 프로퍼티 등을 이용해 통신상태 체크 등을 할 수 있다고는 하나 대부분의 경우는 제이쿼리(jQuery)를 사용해 구현한다.


jQuery에서는 친절하게 $.ajax() 메서드를 제공하여 서버와 비동기식 통신을 할 수 있도록 해준다. ($.get()이나 $.post() 메서드도 있다.)

$('#submit_button').click(function(e){
var params = $("#form01").serialize();
var url = "test01";
$.ajax({
url: url,
type: 'POST',
data: params,
dataType: 'html',
success: function (result) {
if (result){
// 데이타 성공일때 이벤트 작성
}
},
error : function(result){

}
});
e.preventDefault();
});

위와같이 $.ajax([key:value 옵션]) 을 이용해 서버로 통신요청을 하며 콜백함수도 지정해 성공이나 실패시 원하는 작업을 수행할 수도 있다. 다양한 옵션을 줄 수 있으니 원하는 작업이 있을때는 구글링을 해보자.


웹 서버에서는 이에대한 응답을 XML 또는 JSON 형태로 보내주는데, 대부분의 경우에는 JSON이 XML보다 낫다고 한다. (JSON이 XML보다 파싱하기에 좀더 빠르고 쉽다.)


끝.



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

LastModified 헤더를 이용한 파일변경 체크  (0) 2020.12.06
Maven 기본  (0) 2019.11.12
UTC, GMT, Epoch Time  (0) 2019.10.11
META-INF 디렉터리에 대하여  (1) 2018.04.12
TCP/IP  (0) 2017.08.01

+ Recent posts