@DataJpaTest

@DataJpaTest 어노테이션은 JPA 관련 테스트 설정만 로드합니다. DataSource의 설정이 정상적인지, JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능합니다. 그리고 가장 좋은점은.. 무려 내장형 데이터베이스를 사용하여 실제 데이터베이스를 사용하지 않고 테스트 데이터베이스로 테스트할 수 있는.. 개꿀같은 장점이 있습니다.

@DataJpaTest는 기본적으로 @Entity 어노테이션이 적용된 클래스를 스캔하여 스프링 데이터 JPA 저장소를 구성합니다. 만약 최적화한 별도의 데이터소스를 사용하여 테스트하고 싶다면 기본 설정된 데이터소스를 사용하지 않도록 아래와 같이 설정해도 됩니다.

RunWith(SpringRunner.class)
@DataJpaTest
@ActiveProfiles("...")
@AutoConfigureTestDatabase(replace = 
@AutoConfigureTestDatabase.Replace.NONE)
public class JpaTest {

}

@AutoConfigureTestDatabase 어노테이션의 기본 설정값인 Replace.Any를 사용하면 기본적으로 내장된 임베디드 데이터베이스를 사용합니다. 위의 코드에서 Replace.NONE로 설정하면 @ActiveProfiles에 설정한 프로파일 환경값에 따라 데이터 소스가 적용됩니다. yml 파일에서 프로퍼티 설정을 spring.test.database.replace: NONE으로 변경하면 됩니다.

@DataJpaTest는 기본적으로 @Transactional 어노테이션을 포함하고 있습니다. 그래서 테스트가 완료되면 자동으로 롤백하기 때문에 직접 선언적 트렌젝션 어노테이션을 달아줄 필요가 없습니다.

만약에 @Transactional 기능이 필요하지 않다면 아래와 같이 줄 수 있습니다.

@RunWith(SpringRunner.class)
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class JpaTest {
    ...
}

그리고 어떤 테스트 데이터베이스를 사용할 것인지도 선택 할 수 있습니다. spring.test.database.connection: H2와 같이 프로퍼티를 설정하는 방법과 @AutoConfigureTestDatabase(connection = H2) 어노테이션으로 설정하는 방법이 있습니다.

@DataJpaTest에서 EntityManager의 대체제로 만들어진 테스트용 TestEntityManager를 사용하면 persist, flush, find 등과 같은 기본적인 JPA테스트가 가능합니다. 아래 간단하게 도메인 객체에 대한 JPA 테스트를 수행할 수 있게 Book 클래스에 JPA 관련 어노테이션을 추가하고 BookRepository 인터페이스를 생성하였습니다.

Entity

package com.jun.jpacommunity.domain;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {

    @Id
    @GeneratedValue
    private Integer idx;

    private String title;

    private LocalDateTime publishedAt;

    @Builder
    public Book(String title, LocalDateTime publishedAt) {
        this.title = title;
        this.publishedAt = publishedAt;
    }
}

Repository

package com.jun.jpacommunity.repository;

import com.jun.jpacommunity.domain.Book;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Integer> {

}

Test 코드

package com.jun.jpacommunity.repository;


import com.jun.jpacommunity.domain.Board;
import com.jun.jpacommunity.domain.Book;
import com.jun.jpacommunity.domain.Member;
import org.hamcrest.collection.IsEmptyCollection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class JpaTest {

    private final static String BOOT_TEST_TITLE = "Spring Boot Test Book";

    @Autowired
    private TestEntityManager testEntityManager;

    @Autowired
    private BookRepository bookRepository;

    @Test
    public void Book_저장하기_테스트() throws Exception {
        //given
        Book book1 = Book.builder()
                .title("타짜")
                .publishedAt(LocalDateTime.now())
                .build();

        //when
        testEntityManager.persist(book1);

        //then
        assertEquals(book1, testEntityManager.find(Book.class, book1.getIdx()));

     }

     @Test
     public void BookList_저장하고_검색_테스트() throws Exception {

         //given
         Book book1 = Book.builder()
                 .title(BOOT_TEST_TITLE +"1")
                 .publishedAt(LocalDateTime.now())
                 .build();

         testEntityManager.persist(book1);

         Book book2 = Book.builder()
                 .title(BOOT_TEST_TITLE +"2")
                 .publishedAt(LocalDateTime.now())
                 .build();

         testEntityManager.persist(book2);

         Book book3 = Book.builder()
                 .title(BOOT_TEST_TITLE +"3")
                 .publishedAt(LocalDateTime.now())
                 .build();

         testEntityManager.persist(book3);


         //when
         List<Book> bookList = bookRepository.findAll();

         //then
         assertEquals(bookList.size(), 3);
         assertEquals(book1, bookList.get(0));
      }


    @Test
    public void BookList_저장하고_삭제_테스트() throws Exception {

        //given
        Book book1 = Book.builder().title(BOOT_TEST_TITLE + "1").
                publishedAt(LocalDateTime.now()).build();

        testEntityManager.persist(book1);

        Book book2 = Book.builder().title(BOOT_TEST_TITLE + "2").
                publishedAt(LocalDateTime.now()).build();

        testEntityManager.persist(book2);

        //when
        bookRepository.deleteAll();
        //then
        assertThat(bookRepository.findAll(), IsEmptyCollection.empty());
    }
}

요즘에 TDD 방식으로 테스트 하는 습관을 기르기 위해서 인텔리제이에서 제공해주는 라이브 템플릿으로 간단하게 give, when, then으로 테스트 영역을 구분하여 테스트를 수행하였습니다.

  • Book_저장하기_테스트(): testEntityManager로 persist() 기능이 정상 동작되는지 테스트를 수행하였습니다.

  • BookList_저장하고_검색_테스트(): Book 3개를 저장한 후 저장된 Book의 개수가 3개가 맞는지, bookList에 0번째 인덱스에 저장된 book1 객체가 포함 되어있는지 테스트를 수행하였습니다.

  • BookList_저장하고_삭제_테스트(): 저장된 Book 중에서 2개가 제대로 삭제되었는지 테스트를 하였습니다.

참조: https://hyper-cube.io/2017/08/10/spring-boot-test-2/, 처음배우는 스프링 부트2

'SpringFramework > JPA' 카테고리의 다른 글

복합 키와 식별관계 매핑  (0) 2019.10.29
고급매핑 - 상속관계 매핑  (0) 2019.10.28
JPA 다양한 연관관계 매핑  (0) 2019.10.04
JPA 연관관계 매핑기초  (0) 2019.10.04
JPA가 지원하는 엔티티 매핑  (0) 2019.10.04

스트림(Stream)

스트림은 자바 8부터 추가된 컬렉션(배열포함)의 저장 요소를 하나씩 참조해서 람다식(험수적 -스타일)으로 처리할 수 있도록 해주는 반복자이다. 스트림에 대해서 저는 전혀 몰랐었고(자랑이다…), 프로젝트를 진행하면서 구글링할때 다른 개발자분이 스트림으로 작성한 코드와 스프링 부트 책의 예제에서 잠깐 보고 말았던 것인데 따로 공부를 하면서 정말 편하게 컬렉션의 요소들을 처리 할 수 있다는게 너무 좋아서 공부를 해봤습니다.

자바7 이전까지는 List 컬렉션에서 요소를 순차적으로 처리하기 위해 Iterator 반복자를 사용하여 처리하곤 했습니다.

ex)

List<String> list = Arrays.asList("홍길동", "임준영", "자바킴");
Iterator<String> iterator = list.iterator();//반복처리를 위해 Iterator 객체를 얻어옴
while(iterator.hasNext()){ // 해당 컬렉션에 요소가 있는지 확인 있으면 true 리턴
    String name = iterator.next(); //요소를 name 변수에 저장
    System.out.println(name); 
}

위의 코드처럼 iterator.hasNext()로 컬렉션 타입에 요소가 있는지 확인하고 꺼내서 처리를 하지만 스트림을 이용하면 다음처럼 구현할 수가 있습니다.

List<String> list = Arrays.asList("홍길동", "임준영", "자바킴");
Stream<String> stream = list.stream(); //컬렉션의 stream()메소드로 스트림 객체를 얻음 
stream.forEach(name -> System.out.println(name)); //해당 메소드로 컬렉션의 요소 출력

스트림 객체의 forEach 메소드의 구현부를 보면 Consumer 함수적 인터페이스 타입의 매개값을 가집니다.
따라서, 컬렉션의 요소를 소비할 코드를 람다식으로 기술할 수 있습니다.

void forEach(Consumer<? super T> action);

스트림을 사용하기전에 함수적 인터페이스와 람다식에 대해서 간단하게 설명하자면 자바 코드를 간결하고 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계하기 위해 만들어졌고, 이벤트 지향 프로그래밍과 병렬처리를 수행하기 위해 스트림과 마찬가지로 자바 8부터 지원하는 함수적 프로그래밍이라고 생각하면 됩니다.

  • 객체지향 언어 + 함수지향 언어 == 람다식
(s) -> System.out.println(s);  // 매개변수와 실행문으로 구성되어 있습니다.

람다식의 형태는 위의 코드처럼 매개 변수를 가진 코드 블록이지만, 런타임 시에는 구현 객체를 생성합니다.

람다식이 하나의 메소드를 정의하기 때문에 두개 이상의 추상 메소드가 선언된 인터페이스는 익명으로 구현이 불가능합니다. 그렇기 때문에 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타겟타입이 될 수 있는데, 이러한 인터페이스를 함수적 인터페이스라고 부릅니다.

자바에서 제공되는 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스들은 모두 람다식을 이용해서 익명 구현 객체로 표현이 가능합니다. 자바 8부터는 빈번하게 사용되는 함수적 인터페이스는 java.util.function 표준 API 패키지로 제공합니다. 이 패키지에서 제공하는 함수적 인터페이스의 목적은 메소드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위해서입니다.

스트림의 forEach 메소드의 매개값에 대해서 이해하기 위해서 뭔가를 쓰긴했는데…서론이 쓰잘대 없이 길었네요ㅎㅎ… Consumer는 함수적 인터페이스로 특징은 리턴값이 없는 accept() 메소드를 가지고 있습니다. accept()메소드는 단지 매개값을 소비하는 역할만 하는데. 소비하다는 말은 사용만 할뿐 리턴값이 없는 딱히 의미있는 건 아닙니다.

Consumer<String> consumer = t -> { t를 소비하는 실행문; }; //람다식으로 익명구현 객체 생성

대충 이런식으로 매개값을 소비합니다.

이제 본격적으로 스트림에 대해서 알아보겠습니다. 위의 Iterator를 사용한 코드와 Stream을 사용한 코드를 비교해보면 Stream을 사용하는 것이 훨씬 간결해 보입니다.

스트림의 특징

Stream은 Iterator와 비슷한 역할을 하는 반복자이지만, 람다식으로 요소 처리 코드를 제공하는 점과 내부 반복자를 사용하므로 병렬 처리가 쉽다는 점 그리고 중간 처리와 최종 처리 작업을 수행하는 점에서 많은 차이를 가지고 있습니다.

Stream이 제공하는 대부분의 요소 처리 메소드는 함수적 인터페이스 매개 타입을 가지기 때문에 람다식 또는 메소드 참조를 이용해서 요소 처리 내용을 매개값으로 전달할 수 있습니다.

아래 이해하기 쉬운 좋은 예제가 있습니다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class LamdaExpressionsExample {

    public static void main(String[] args) {

        List<Student> list = Arrays.asList(
                new Student("임준영",100),
                new Student("노성운",92)
        );

        Stream<Student> stream = list.stream(); //스트림 얻기
        stream.forEach(s -> {//List컬렉션에서 Student를 가져와 람다식의 매개값으로 제공.
            String name = s.getName();
            int score = s.getScore();
            System.out.println("학생이름: " + name + " " + "점수: " + score);
        });
    }
}
public class Student {

    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }
} 

스크린샷 2019-10-05 오전 1 11 58

스트림의 강력한 장점 중 하나는 내부 반복자 패턴이다. Iterator는 컬렉션의 요소를 가져오는 것에서부터 처리하는 것까지 모두 개발자가 작성해야 하지만, 스트림은 람다식으로 요소 처리 내용만 전달할 뿐, 반복은 컬렉션 내부에서 일어납니다. 스트림을 이용하면 코드도 간결해지지만, 무엇보다도 요소의 병렬 처리가 컬렉션 내부에서 처리되므로 일석이조의 효과를 누릴수 있습니다 ㅎㅎ.

또 다른 장점은 제가 스트림에 대해서 가장 많은 매력을 느낀점인데…
중간 처리와 최종처리가 가능하다는 점입니다!!!
이게 무슨 소리냐면 스트림은 컬렉션의 요소에 대해 중간 처리와 최종 처리를 수행할 수 있는데, 중간 처리에서는 매핑, 필터링, 정렬을 수행하고 최종처리에는 반복, 카운팅, 평균, 총합 등의 집계처리를 수행합니다.

예를 들어 학생 객체를 요소로 가지는 컬렉션이 있다고 가정해봅시다. 중간 처리에서는 학생의 점수를 뽑아내고, 최정처리에서는 점수의 평균 값을 산출합니다.

스크린샷 2019-10-05 오전 1 12 09

중간처리: 학생의 개별 점수를 뽑아낸다. (Student 객체를 점수로 매핑)

최종처리: 점수의 평균값을 산출한다.(집계)

여윽시…코드로 봐봅시다.

mport java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class MapAndReduceExample {
    public static void main(String[] args) {

        List<Student> studentList = Arrays.asList(
          new Student("임준영", 10),
          new Student("임광빈", 20),
          new Student("박찬욱", 30)
        );

        double avg = studentList.stream()
        .mapToInt(Student::getScore)
        .average()
        .getAsDouble();  //한줄로 평균을 구하는... 스트림의 위엄....

        System.out.println("평균 점수: " + avg);
        Stream<Student> stream = studentList.stream();
        stream.forEach(s -> System.out.println(s.getName()));
    }
}

스트림과 람다식을 사용하면 정말 간결하게 해당 객체의 점수를 뽑아내서 평균을 구할 수 있습니다.

List 컬렉션의 studentList변수에는 Student 타입의 객체 요소들을 가지고 있습니다.
스트림 객체를 얻어온 다음에 체인방식으로 mapToInt 메소드를 이용하여 중간처리에서 해당객체를 score 필드값으로 매핑을 합니다.

IntStream mapToInt(ToIntFunction<? super T> mapper);

마찬가지로 해당 메소드의 구현부를 보면 함수적 인터페이스를 매개값으로 받습니다.
ToIntFunction 함수적 인터페이스는 객체 T를 매개로 받아서 int 타입으로 매핑합니다.
이런식으로 컬렉션 내부적으로 스트림이 반복처리를 하고 중간처리에서 매핑한 score 필드값들로 score의 평균값을 산출하는 과정입니다. 정말 unbelievable 합니다.

스트림에서 제공하는 메소드에 대해서 아래 URL을 참조해주세요.
스트림 처리 메소드 - https://webcoding-start.tistory.com/48

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
@Annotation 이란?  (0) 2019.10.05
클린코드 OOP의 장점  (0) 2019.10.05
빌더 패턴(Builder Pattern)  (0) 2019.10.05

자료구조와 함꼐 배우는 알고리즘 입문(자바편) 책을 보다가 우연히 전에 사놓고 읽지 않는 이것이 자바다라는 책을 보게 되었습니다. 사실 점점 더 어려워지는 알고리즘 공부가 싫어서 만든 핑계는?? 아닙니다 하하… 이 책은 총 2권으로 구성 되어있고, 대학교때 시중에서 자바와 관련된 책 중에서 나는 Java를 공부한적이 없어요라는 책을 읽으면서 어느정도 자바의 문법과 개념에 대해서 공부했었는데… 내가 자바에 대해서 얼마나 기초지식을 얼마나 기억하고있는지 심심해서… 한번 정독을 해보았습니다.
그 중에서도 어노테이션이에 대한 내용이 나와있길래 자세히 읽어보고 이해하게 되었습니다.

@Annotation

어노테이션에 대해서 떠오르는건 스프링 MVC로 프로젝트를 진행할때 어노테이션은 단순히 lombok을 사용하여 @Getter, @Setter를 특정 클래스 위에 기술하여 클래스 파일 생성시 getter, setter를 알아서 자동으로 만들어주는 아주 편리한녀석??으로만 생각했었습니다.
또 한 부모클래스나 인터페이스 같은 상위타입의 메소드를 재정의할 때 해당메소드 위에 @Override를 표시하는 정도만 알고먼 있었습니다. 부끄럽지만 Java 1.5부터 제공하는 어노테이션에 대해서 예제를 통해서 정확하게 무엇을 하고 어떻게 Customizing 해서 사용하는지 리뷰를 해보겠습니다.

어노테이션 개념

어노테이션(Annotation)은 메타데이터(metadata)라고 볼 수 있습니다. 메타데이터는 Application이 처리하는 데이터가 아니고, 컴파일 과정과 런타임 과정에서 코드를 어떻게 컴파일하고 처리할것인지를 알려주는 정보입니다. 어노테이션은 다음과 같은 형태로 작성됩니다.

@AnnotationName

어노테이션은 다음 세가지 용도로 사용됩니다.

  • 컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공
  • 소프트웨어 개발 툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보를 제공
  • 실행 시(런타임 시) 특정 기능을 실행하도록 정보를 제공

첫번째로 컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공하는 대표적인 예는 위에어 언급한 @Override 어노테이션 입니다. 예를 들어서 부모클래스의 메소드를 재정의하여 사용할때 컴파일 시 상위타입(부모클래스, 인터페이스)에 해당 메소드가 존재하는지 확인하고 만약 존재하지 않는다면 컴파일 에러를 발생시킵니다. 어노테이션은 빌드 시 자동으로 XML 설정 파일을 생성하거나, 배포를 위해 JAR 압축 파일을 생성하는데에도 사용됩니다. 그리고 실행 시 클래스의 역할을 정의하기도 합니다.

어노테이션 정의

어노테이션 타입을 정의하는 방법은 인터페이스를 정의하는 것과 유사합니다.

public @interface AnnotationName{

}

이렇게 정의한 어노테이션은 코드에서 다음과 같이 사용합니다.

@AnnotationName

어노테이션은 클래스의 필드처럼 엘리먼트라는 녀석을 맴버로 가질 수 있습니다. 각 엘리먼트는 타입과 이름으로 구성되며, 디폴트 값을 가질 수 있습니다. 엘리먼트 타입으로는 int나 double과 같은 원시 타입이나 String, Class 타입, 그리고 이들의 배열 타입을 사용할 수 있습니다.

public @interface AnnotationName{
    String elementName1();        // 엘리먼트 선언
    int elementName2() default 5;
}

이렇게 정의한 어노테이션을 코드에서 적용할 때에는 다음과 같이 기술합니다.

@Annotation(elementName1="값", elementName2=3);
또는
@AnnotationName(elementName1 = "값");

elementName1은 디폴트 값이 없기 때문에 반드시 값을 기술해야하고, elementName2는 디폴트 값이 있기 때문에 생략 가능합니다. 어노테이션은 기본 엘리먼트인 value를 가질 수 있습니다.

Value 엘리먼트를 가진 어노테이션을 코드에서 적용할 때에는 다음과 같이 값만 기술할 수 있습니다.
이 값은 기본 엘리먼트인 value 값으로 자동 설정됩니다.

@Annotation("값");

만약 value 엘리먼트와 다른 엘리먼트의 값을 동시에 주고 싶다면 다음과 같이 정상적인 방법으로 지정하면 됩니다.

@AnnotationName(value="값",  elementName=3);

@Annotation 적용 대상

스크린샷 2019-10-05 오전 1 02 58

@Target

어노테이션이 적용될 대상을 지정할 때에는 @Target 어노테이션을 사용합니다. @Target의 기본 엘리먼트인 value는 ElementType 배열을 값으로 가진다. 이것은 어노테이션이 적용될 대상을 복수개로 지정하기 위해서 입니다.

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotatiionName{

}

어노테이션 유지 정책

어노테이션 정의 시 한 가지 더 추가해야 할 내용은 사용 용도에 따라 @AnnotationName을 어느 범위까지 유지할 것인지 지정해야 합니다. 쉽게 설명하면 소스상에만 유지할 건지, 컴파일된 클래스까지 유지할 건지, 런타임 시에도 유지할 건지를 지정해야 합니다. 어노테이션 유지 정책은 java.lang.annotation.RetentionPolicy 열거 상수로 다음과 같이 정의되어 있습니다.

스크린샷 2019-10-05 오전 1 03 05

리플렉션(Reflection)이란 런타임 시에 클래스의 메타 정보를 얻는 기능을 말합니다. 예를 들어 클래스가 가지고 있는 필드가 무엇인지, 어떤 생성자를 갖고 있는지, 어떤 메소드를 가지고 있는지, 적용된 어노테이션이 무엇인지 알아내는 것이 리플렉션 입니다. 리플렉션을 이용해서 런타임 시에 어노테이션 정보를 얻으려면 어노테이션 유지 정채을 RUNTIME으로 설정해야 합니다. 어노테이션 유지 정책을 지정할 때에는 @Retention 어노테이션을 사용합니다.

@Annotation 예제 코드

어노테이션과 리플렉션을 이용해서 간단한 예제를 만들어 보도록 합시다. 다음은 각 메소드의 실행 내용을 구분선으로 분리해서 콘솔에 출력하도록 하는 PrintAnnotation입니다.

Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation {
    String value() default "-";
    int number() default 15;
}

위의 코드에서 @Target은 메소드에만 적용하였고, @Retention은 런타임 시까지 어노테이션 정보를 유지하도록 하였습니다. 기본 엘리먼트 value는 구분선에 사용될 문자이고, number는 반복 출력 횟수입니다. 각각 디폴트 값으로 "-"와 15를 주었습니다. 다음은 PrintAnnotation을 적용한 Service 클래스 입니다.

public class Service {

    @PrintAnnotation // 해당 어노테이션은 엘리먼트의 기본값으로 설정
    public void method1(){
        System.out.println("실행 내용1");
    }

    @PrintAnnotation("*") // 엘리먼트 value 값을 "*"로 설정
    public void method2(){
        System.out.println("실행 내용2");
    }

    @PrintAnnotation(value="#", number=20) // value 값을 "#", number 20 설정
    public void method3(){
        System.out.println("실행 내용3");
    }
}

다음 PrintAnnotationExample 클래스는 리플렉션을 이용해서 Service 클래스에 적용된 어노테이션 정보를 읽고 엘리먼트 값에 따라 출력할 문자와 출력 횟수를 콘솔에 출력한 후, 해당 메소드를 호출합니다.

public class PrintAnnotationExample {
    public static void main(String[] args) {
        //Service 클래스의 메소드 정보를 Method 배열로 리턴합니다.
        Method[] declaredMethods = Service.class.getDeclaredMethods();
        //메소드 객체를 하나씩 처리
        for (Method method : declaredMethods) {
            if (method.isAnnotationPresent(PrintAnnotation.class)) {
                //객체 얻기
                PrintAnnotation printAnnotation = method.getAnnotation(PrintAnnotation.class);

                //메소드 이름 출력
                System.out.println("[" + method.getName() + "] ");

                //구분선 출력
                for (int i = 0; i < printAnnotation.number(); i++) {
                    System.out.print(printAnnotation.value());
                }
                System.out.println();


                try {
                    method.invoke(new Service());
                } catch (Exception e) {}
                    System.out.println();
            }
        }
    }
}

위의 isAnnotationPresent 메소드는 지정한 어노테이션이 적용되었는지 여부, Class에서 호출했을때 상위 클래스에 적용된 경우도 true를 리턴합니다.
method.invoke(new Service())는 Service 객체를 생성하고 생성된 Service 객체의 메소드를 호출하는 코드입니다. 메모리에 해당 객체가 있어야만 메소드를 호출이 가능하기 때문이죠.

이러한 어노테이션의 기본적인 개념에 대해서 이해를 하고 @Getter 어노테이션을 살펴보니 전에는 몰랐던 부분이 조금이나마 이해가 되었습니다. 실습을 통해서 리플렉션과 어노테이션에 대해서 깊게는 아니지만 보이지 않았던 부분이 조금이나마 보이게되서 정말 보람찬 하루였습니다.ㅎㅎ

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
Stream(스트림)  (0) 2019.10.05
클린코드 OOP의 장점  (0) 2019.10.05
빌더 패턴(Builder Pattern)  (0) 2019.10.05

IDE는 이클립스만 사용해보다가 최근에 Mac OS를 사용하면서 Intellij로 갈아타게 되었습니다. 물론 무엇을 쓰든 상관없었지만, 트렌드에 쓰잘대 없이 민감한 제가… 큰 마음먹고 한번 사용해보았습니다. 아직은 익숙하지가 않지만 간단하게 Intellij에서 스프링 mvc를 Mybatis 모듈과 연동하여 웹 어플리케이션을 구현해보는 시간을 리뷰해 보았습니다.

먼저, 꽤 저렴하지 않는 금전으로 Intellij Ultimate 1년 구독신청 하였기 때문에 해당 IDE로 실행을 하겠습니다.

Intellij를 사용하여 Spring-Mybatis 연동

스크린샷 2019-10-05 오전 12 41 42

미리 만들어놓은 프로젝트의 구조는 다음과 같습니다. 생각보다 복잡해서… 프로젝트 생성과정은 생략하였습니다. 빌드 도구로는 역시… 이클립스에서 사용해왔던 maven을 이용하여 pom.xml 파일에서 의존성 라이브러리들을 관리합니다.

스크린샷 2019-10-05 오전 12 41 51

위의 프로젝트 구조에서 database.properties 파일과 classes, views디렉토리를 제외하고는 Intellij에서 maven과 스프링 mvc로 프로젝트를 생성하여 보여주는 기본구조입니다.

스프링-Mybatis 모듈 연동을 구현하기 전에 간단하게 프로젝트 구조에 대해서 리뷰해보겠습니다.
웹 어플리케이션이 톰캣에서 동작할때 가장 먼저 web.xml파일을 참조하는데 설정내용은 이클립스와 크게 다르지 않습니다.

스프링 설정파일

web.xml 파일

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

dispatcher-servlet.xml 파일

<?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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

  <!-- 아래 annotation-driven 태그는 HandlerMapping, HandlerAdapter를 Bean으로 등록하여 요청 url를 controller와 매칭시켜준다.-->
    <mvc:annotation-driven/>

    <mvc:resources mapping="/resources/**" location="/resources/"/>

  <!-- 특정 패키지안의 클래스들을 스캔하고, annotation을 확인 후 bean 인스턴스를 생성한다.-->
    <context:component-scan base-package="com.jun"/>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

기존의 이클립스와의 차이점은 context파일 명이 servlet-context.xml, root-context.xml 파일이 아니고 dispatcher-servlet.xml, applicationContext.xml 입니다.

dispatcher-servlet.xml파일에서 view를 검색하는 역할을 하는 ViewResolver를 빈으로 등록하고 js,css같은 정적파일들은 WAS가 처리하지 않고, WEB 서버에게 위임합니다. 스프링 mvc를 사용한다고 하면, 대표적으로 DispatcherServlet를 등록하고, 모든 요청을 받게 설정을 하는데 /resources/** 로 요청이 오면 /resources/로 매핑시켜주어서 바로 처리할 수 있도록 설정하였습니다.

이제 간단하게 요청 url에 특정 id 값을 보내면 Mybatis에서 sqlSession 객체를 이용하여 해당하는 값을 DB 테이블에서 검색하여 view 화면에서 보여주는 예제를 구현해보겠습니다.

이러한 작업을 위해서… 역시 설정들을 간단한?? 설정을 해주면 됩니다.
그전에 pom.xml에서 mybatis, mybatis-spring, mysql-connector-java 의존성 라이브러리들을 pom.xml에 작성하여 해당 dependency들을 받아와야 됩니다.

mybatis는 sql 명령어를 관리해주는 편리한 라이브러리이고, 유지보수성이나 효율성이 기존에 jdbc에서 사용해왔던 방식보다 훨씬 뛰어납니다.

<!-- MyBatis, MyBatis-Spring, Spring-JDBC -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>

            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

mybatis와 mybatis를 연동하는 스프링 그리고 스프링과 jdbc를 연결하는 라이브러리입니다.

mybatis는 필연적으로 데이터베이스를 이용하는 sql문을 설정하는 라이브러리이기에 데이터베이스 개발환경에 꼭 필요하고 스프링 jdbc와 그를 연결하는 mybatis-spring 라이브러리가 필수적입니다.

아래의 그림이 설명이 잘돼어있는거 같아서 참고하였습니다.

스크린샷 2019-10-05 오전 12 42 05

이제 applicationContext.xml파일에서 데이터베이스 관련 설정을 해보겠습니다.

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

<context:property-placeholder location\="/WEB-INF/database.properties"/>  
<!--<context:component-scan base-package="com.jun"/>-->  

<!-- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
<property name="url" value="${jdbc.url}"/>  
<property name="username" value="${jdbc.username}"/>  
<property name="password" value="${jdbc.password}"/>  
</bean>-->  

<bean id\="dataSource" class\="org.springframework.jdbc.datasource.DriverManagerDataSource"\>  
<property name\="driverClassName" value\="com.mysql.jdbc.Driver"/>  
<property name\="url" value\="${jdbc.url}"/>  
<property name\="username" value\="${jdbc.username}"/>  
<property name\="password" value\="${jdbc.password}"/>  
</bean\>  
<!-- Mysql <-> Mybatis를 연결해주는 객체 -->  
<bean id\="sqlSessionFactory" class\="org.mybatis.spring.SqlSessionFactoryBean"\>  
<property name\="dataSource" ref\="dataSource"/>  
<property name\="configLocation" value\="classpath:/mybatis-config.xml"/>  
<property name\="mapperLocations" value\="classpath:/mappers/\*.xml"/>  
</bean\>  

<bean id\="sqlSession" class\="org.mybatis.spring.SqlSessionTemplate"\>  
<constructor-arg ref\="sqlSessionFactory"\></constructor-arg\>  
</bean\>  
</beans\>

DB url, username, password 같은 정보는 보안상 따로 파일을 만들어서 변수를 사용하여 입력하는게 좋은 방법입니다.
<context:property-placeholder location="/WEB-INF/database.properties"/> 태그로 database.properties 파일을 해당 xml파일로 import를 하여 dataSource 빈에 property에 변수를 값으로 넣어줍니다.

sqlSessionFactory 빈에는 mybatis 설정파일과 sql문을 관리하는 mapper xml파일의 경로와 데이터베이스 연결정보들을 가지고 있는 dataSource 객체를 property 값으로 가지고 있습니다.

mybatis-config.xml, member-config.xml 파일은 src/main/resources 디렉토리 밑에 생성하여 만든 설정파일로써 해당경로를 클래스패스로 인식하기 때문에 값을 넣어줄때 classpath:를 써줘야 인식합니다. 안그러면 에러가 발생합니다.

sqlSessionFactory는 데이터베이스 연결과 sql문 실행에 대한 모든 것을 가진 가장 중요한 객체입니다.

여기까지 스프링 웹 어플리케이션에서 DB를 연결하기 위한 설정하는 방법입니다.
이제 자바코드를 이용하여 해당 웹 어플리케이션을 구현해보겠습니다.

  1. Controller
package com.jun.controller;

import com.jun.dto.MemberDto;
import com.jun.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {


    @Autowired
    private MemberService memberService;

    @RequestMapping("/home")
    public String home(){

        return "homepage";
    }

    @RequestMapping("/index")
    public String index(){

        return "index";
    }

    @RequestMapping("/loginForm")
    public String loginForm(){

        return "loginForm";
    }
    //해당 url/id 값으로 요청이 들어오면 loginResultView 메소드 실행
    @RequestMapping("/loginResultView/{id}")
    public String loginTestResult(@PathVariable String id, Model model){

        MemberDto dto = memberService.selectMember(id);

        model.addAttribute("member", dto);

        return "loginResultView";
    }
}
  1. DTO
package com.jun.dto;

public class MemberDto {

    private String id;
    private String name;


    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  1. DAO
package com.jun.dao;

import com.jun.dto.MemberDto;
import com.jun.mapper.MemberMapper;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class MemberDao {

    @Autowired
    private SqlSession sqlSession;

    public MemberDto selectMember(final String id){

        MemberMapper mapper = sqlSession.getMapper(MemberMapper.class);

        MemberDto memberDto = mapper.selectMember(id);

        return  memberDto;
    }
}
  1. service
package com.jun.service;

import com.jun.dto.MemberDto;

public interface MemberService {
    public MemberDto selectMember(String id);
}
package com.jun.service;

import com.jun.dao.MemberDao;
import com.jun.dto.MemberDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    private MemberDao memberDao;

    public MemberDto selectMember(String id) {

        MemberDto memberDto = memberDao.selectMember(id);

        return memberDto;
    }
}

sql문에서 테이블에서 select로 단일조회하여 저장할 때도 사용하는 용도로 작성한 파일입니다.

  1. mapper
package com.jun.mapper;

import com.jun.dto.MemberDto;

public interface MemberMapper {
    MemberDto selectMember(String id);
}

sqlSession 객체에서 해당 interface를 구현하여 MemberMapper 객체를 이용하여
member-mapper.xml파일에 정의되어 있는 sql파일을 실행하는 책임을 가지고 있습니다.

  1. mapper-mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jun.mapper.MemberMapper">
    <select id="selectMember" parameterType="String" resultType="memberDto">
        SELECT name, id from TEST1 WHERE id =#{id}
    </select>
</mapper>

접근하고자 하는 테이블의 sql 명령어들을 정의한 xml 파일입니다.

  1. mapper-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <typeAliases>
        <typeAlias alias="memberDto" type="com.jun.dto.MemberDto"/>
    </typeAliases>
</configuration>

mybatis-config.xml 파일은 Mybatis에 별도의 설정들을 정의합니다.
member-mapper.xml에서 정의된 sql문의 리턴타입을 dto로 사용할 경우에 태그를 사용하면 풀 패키지명 없이 간단하게 memberDto를 리턴타입으로 사용할 수 있습니다.

mysql을 실행하여 임의로 해당 테이블을 dto(Member)와 같은 타입으로 셍상히였습니다.
간단한 spring-mybatis 예제이기 때문에 따로 password는 암호화하여 넣지는 않았습니다.

스크린샷 2019-10-05 오전 12 42 19

위의 코드들을 작성했으면 이제 database에서 해당 값을 가져오는지 실행결과를 보겠습니다.

스크린샷 2019-10-05 오전 12 51 27

대충 작성하였지만 스프링 부트를 배우기 전에 다시한번 복습한다는 생각으로 mybatis-spring 연동예제를 구현해보았습니다. 처음에는 eclipse와 환경도 다르고 library 가져오는 방법도 약간 달라서 어려움이 있었지만… 역시 코딩은 삽질이 답인거 같네요. 제 부족한 점을 다시한번 짚어볼 수 있는 유익한 시간이였습니다.

클린 코더스 강의 1.OOP

백명석 선생님의 클린코드라는 강의가 나온지 6년이나 되었지만, 이제 막 프로그래밍 세계에 입문한 저한테는 정말 배울게 많은 강의였습니다. 가장 기억에 남는 말씀은 개발자들이 많이하는 실수는 객체지향 프로그래밍을 할때 기능을 중심으로 생각하는게 아니라 해당 클래스가 가지고 있는 데이터 중심으로 사고를 한다는거 였습니다.

에를들어 글이나 기사를 작성하는 서비스에 대한 네이밍을 WriteArticleService가 아니고 ArticleService라고 작명한는 것도 기능이 아니라 데이터 중심으로 생각을 해서 발생하는 문제입니다.

1강의 핵심은 돌아가는 코드도 중요하지만 사람이 읽을 수 있는 코드에 초점을 맞춘 강의였습니다. 리펙토링을 하는 습관을 항상 가져야되고… 아무리 바쁘더라도 리펙토링을 미루면 안됩니다. 개발을 빨리 한다고 해도 코드가 지저분하고 돌아가는데만 신경을 쓴다면 나중에 유지보수하는데 상당한 골치를 겪을 확률이 높습니다.

이번 시간에는 공통된 데이터나 프로세스를 제공하는 객체들을 하나의 타입(인터페이스)로 추상화 하는 코드에 대해서 배웠습니다.

스크린샷 2019-10-05 오전 12 29 16

위의 클래스 다이어그램에서 해당 객체들의 공통점은 바로 로그를 수집하고 iterator() 메소드를 이용해서 수집한 로그를 반복처리하는 역할을 하는 객체입니다.

만약 이 로직을 변경하지 않고 로그 수집 방법을 변경하기 위해서는 인터페이스를 재사용 하는 것입니다. 인터페이스를 재사용하면 Client는 구현 변경에 대해서 영향을 받지 않습니다. 한마디로 추상화를 통해 유연함을 얻기 위한 규칙입니다.

concrete class를 직접 사용하는 경우

public class FlowController{

    private final Parser parser = new Parser();

    public void process(){
        FileLogCollector collector = new FileCollector();

        FileLogSet logset = collector.collect();

        Iterator<String> it = logSet.iterate();

        FileLogWriter writer = new FileLogWriter();

        for(String line = it.next; line != null;){
            String parsedLine = parser.parse(line);
            writer.write(parsedLine);
        }
    }
}

인터페이스 추상화

public class FlowController{

    private final Parser parser = new Parser();

    public void process(){
        LogCollector collector = new FileCollector();

        LogSet logset = collector.collect();

        Iterator<String> it = logSet.iterate();

        LogWriter writer = new FileLogWriter();

        for(String line = it.next; line != null;){
            String parsedLine = parser.parse(line);
            writer.write(parsedLine);
        }
    }
}

위의 소스에서는 LogCollector, LogSet을 interface로 두어서 타입을 추상화 하여 클라이언트에게 구체적인 모습은 드러내지 않고 변경을 유연하다는 걸 보여줍니다.

아래에 백명석 강사님이 설명하신 변경에 유여한 코드를 따라서 만들어본 예제입니다.
mills 단위로 경과시간을 구해주는 ProceuduralStopWatch가 클래스를 설계하였습니다.

// 스탑워치의 역할을 해야하는 클래스
public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;

    public long getElapsedTime(){
        return stopTime - startTime;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();
        //mills 단위로 인스턴스 변수 초기화
        stopWatch.startTime = System.currentTimeMillis();

        doSomething();

        stopWatch.stopTime = System.currentTimeMillis();

         assertThat(elapsedTime, is(greaterThanOrEqualTo(expectedElapsedTime);

    }
}

위의 코드를 보면 doSomething() 메소드가 얼마나 걸리는지 밀리타임으로 경과된 시간을 구하는 프로그램입니다. 만약 여기서 nano 단위로 경과된 시간을 구하라는 요구사항이 추가 된다면 데이터 구조 변경이 유발됩니다.

public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;
    private long startNanoTime;
    private long stopNanoTime;

    public long getElapsedTime(){
        return stopTime - startTime;
    }

    public long getElapsedNanoTime(){
        return stopNanoTime - startNanoTime;
    }   
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();
        //mills 단위로 인스턴스 변수 초기화
        stopWatch.startTime = System.nanoTime();

        doSomething();

        stopWatch.stopTime = System.nanoTime();

        long elapsedTime = stopWatch.getElapsedNanoTime();
        assertThat(elapsedTime, is(greaterThanOrEqualTo(expectedElapsedTime * (long)pow(10, 6))));
    }
}

nano 단위의 경과 시간을 구하는 기능이 추가되었을 뿐인데 해당 데이터를 사용하는 모든 코드를 수정해야 합니다. 프로젝트 규모가 커질수록 이렇게 수정하는데 많은 시간이 할애됩니다.

객체지향으로 해당 클래스를 설계한다면 다음과 같이 클라이언트에 영향을 안마치는 코드를 짤수가 있습니다.

public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;


    public void start(){
        this.startTime = System.nanoTime();
    }

    public void stop(){
        this.stopTime = System.nanoTime();
    }

    public Time getElapsedTime(){
        return new Time(stopTime - startTime);
    }
}
public class Time(){

    private long nano;

    public Time(long nano){
        this.nano = nano;
    }

    public void getMilliTime(){
        return (long)(nano / pow(10, 6));
    }

    public void geNanoTime(){
        return nano;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();

        // startTime 필드에 값을 할당하지 않고 기능 실행
        stopWatch.start();

        doSomething();

        // stopTime 필드에 값을 할당하지 않고 기능 실행
        stopWatch.stop();

        Time time = stopWatch.getElapsedTime();    

         assertThat(time.getNanoTime, is(greaterThanOrEqualTo(expectedElapsedTime * (long)pow(10, 6))));

    }
}

이렇게 내부적으로 구현 내용을 감추게 되면 클라이언트에 영향을 안 미치게 됩니다.
Tell, Don’t Ask라는 뜻으로 데이터를 요청해서 변경하고 저장하라고 하지말고 무슨 기능을 실행하라는 뜻입니다. 데이터를 잘 알고 있는 객체에게 기능을 수행하라고 요청하면 클라이언트 입장에서는 그 객체가 어떤 방법을 수행해도 원하는 값만 잘 던져주면 되는거죠…

마지막으로 객체는 각각 역할을 가지고 있습니다. 역할이란 객체가 가지는 책임들의 집합을 의미합니다. 항상 객체지향적으로 사고하는 습 관을 기르기 위해서 데이터보다 기능중심으로 먼저 생각해야 된다는걸 숙지해야 합니다.

참조: 백명석의 클린코드

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
Stream(스트림)  (0) 2019.10.05
@Annotation 이란?  (0) 2019.10.05
빌더 패턴(Builder Pattern)  (0) 2019.10.05

빌더 패턴(Builder Pattern)

빌더 패턴은 객체를 생성할 때 흔하게 사용하는 패턴입니다.
다음과 같은 코드 스타일로 객체를 생성하는 코드가 있다면, 빌더 패턴을 사용했다고 할 수 있습니다.

Member.Builder builder = new Member.Builder("sa1341", "junyoung");
       builder.age(28)
              .hobby("soccer");

        Member member = builder.build();

언제 Builder 패턴을 사용하는지는 Effective Java에는 다음과 같이 설명합니다.

규칙 2. 생성자 인자가 많을 때는 Builder 패턴 적용을 고라하라.

이펙티브 자바에서 말하는 빌더 패턴은 객체 생성을 깔끔하고 유연하게 하기 위한 기법입니다.

객체를 생성하는 방법을 3가지 정도 소개하고 있습니다.

  • 점층적 생성자 패턴(telescoping constructor pattern)
  • 자바빈 패턴
  • 빌더 패턴

점층적 패턴

먼저 점층적 생성자 패턴을 만드는 방법은 다음과 같습니다.

  1. 필수 인자를 받는 필수 생성자를 하나 만듭니다.
  2. 1개의 선택적 인자를 받는 생성자를 추가합니다.
  3. 2개의 선택적 인자를 받는 생성자를 추가합니다.
  4. 반복 수행
  5. 모든 선택적 인자를 다 받는 생성자를 추가합니다.
//점층적 생성자 패턴 코드의 예: 회원가입 관련 코드
public class Member{

    private final String id;
    private final String name;
    private final String hobby;

    //필수 생성자
    public Member(String id){
        this(id, "이름 비공개", "취미 비공개");
    }

    // 1개의 선택적 인자를 받는 생성자
    public Member(String id, String name){
        this(id, name, "취미 비공개");
    }

    //모든 선택적 인자를 다 받는 생성자
    public Member(String id, String name, String hobby){
        this.id = id;
        this.name = name;
        this.hobby = hobby;
    }
}

장점

new Member(“sa1341”, “이름 비공개”, “취미 비공개”) 같은 호출이 빈번하게 일어난다면, new Member(“sa1341”)로 대체할 수 있습니다.

단점

  • 다른 생성자를 호출하는 생성자가 많으므로, 인자가 추가되는 일이 발생하면 코드를 수정하기 어렵습니다. 또한 코드 가독성도 떨어집니 다.

  • 특히 인자수가 많을 때 호출 코드만 봐서는 의미를 알기 어렵습니다.

//호출 코드만으로는 각 인자의 의미를 알기 어렵습니다.
Car car = new Car(1,30.2,5,7,8,6,4);
Car car = new Car(3,35,210,24);
Car car = new Car(230,71);

자바빈 패턴

점층적 패턴의 대안으로 등장한 자바빈 패턴을 소개합니다.
이 패턴은 setter 메서드를 이용해 생성 코드를 읽기 좋게 만드는 것입니다.

Car car = new Car();
car.setName("Hyundai");
car.setNumber(240);

장점

  • 이제 각 인자의 의미를 파악하기 쉬워졌습니다.
  • 복잡하게 여러 개의 생성자를 만들지 않아도 됩니다.

단점

  • 객체의 일관성이 깨집니다.
    1회의 호출로 객체 생성이 끝나지 않습니다.
    즉 한번에 생성하지 않고 생성한 객체에 값을 계속 셋팅해주고 있습니다.

  • setter 메서드가 있으므로 변경 불가능(immutable)클래스를 만들 수가 없습니다.
    스레드 안전성을 확보하려면 점층적 생성자 패턴보다 많은 일을 해야 합니다.

빌더 패턴(Effective Java 스타일)

public class Member {

    private final String id;
    private final String name;
    private final int age;
    private final String hobbyy;

    public static class Builder {

        private final String id;
        private final String name;
        private int age;
        private String hobby;


        public Builder(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public Builder age(int age){
            this.age = age;
            return this;
        }

        public Builder hobby(String hobby){
            this.hobby = hobby;
            return this;
        }

        public Member build() {
            return new Member(this);
        }

    }

    public Member(Builder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.age = builder.age;
        this.hobbyy = builder.hobby;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getHobbyy() {
        return hobbyy;
    }
}

위와 같이 하면 다음과 같이 객체를 생성할 수 있습니다.

Member.Builder builder = new Member.Builder("sa1341", "junyoung");
        builder.age(28)
               .hobby("soccer");

         Member member = builder.build();

아래와 같이 사용할 수도 있습니다.

//각 줄마다 builder를 타이핑 하지 않아도 되어 편리합니다.
Member member = new Member.Builder("sa1341","junyoung") // 필수값 입력
    .age(28)
    .hobby("농구")
    .build(); // build() 메소드가 객체를 생성해 돌려줍니다.

장점

  • 각 인자가 어떤 의미인지 알기 쉽습니다.
  • setter 메소드가 없으므로 변경 불가능한 객체를 만들 수 있습니다.
  • 한번에 객체를 생성하므로 객체 일관성이 깨지지 않습니다.
  • build() 함수가 잘못된 값이 입력되었는지 검증하게 할 수도 있습니다.
참조: https://johngrib.github.io/wiki/builder-pattern/ (기계인간 종립)

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
Stream(스트림)  (0) 2019.10.05
@Annotation 이란?  (0) 2019.10.05
클린코드 OOP의 장점  (0) 2019.10.05

+ Recent posts