QueryDsl이란?

  • 오픈소스 프로젝트다.
  • 일반적으로 복잡한 Creteria를 대체하는 JPQL 빌더다.
  • JPA의 표준스펙이 아니므로 약간의 설정이 더 필요하다.
  • 복잡한 쿼리와 동적쿼리를 깔쌈하게 해결해준다.
  • 쿼리를 자바 코드로 작성할 수 있다. 따라서 문법오류를 컴파일단계에서 잡아줄 수 있다.

JPQL vs QueryDSL

JPQL

String username = "gracelove"
String query = "select m from Member m"+
 "where m.username = :username";

List<Member> result = em.createQuery(query, Member.class)
                            .getResult.List();
  • 이 코드에 문제가 있다. 쉽게 발견할 수 있는가?
    • String query에 가독성을 위해 개행을 줬다. 이걸 한 줄로 풀어보자.
    • select m from Member mwhere m.username = :username
    • 한 줄로 풀어보니, "...m where..."가 아니라 "...mwhere..."다.
    • 컴파일러가 잡아주지도 않는다. 런타임때 이 쿼리를 실행시키면 그제서야 에러가 난다.

QueryDSL

String username = "gracelove
List<Member> result = queryFactory
                            .select(member)
                            .from(member)
                            .where(member.username.eq(username))
                            .fetch();
  • 자바 코드기 때문에 ide의 도움을 받을 수 있다. 또 컴파일 단계에서 오류를 발견할 수 있다. 게다가 자바 코드다 보니, 메서드추출도 할 수 있어서 재사용성 또한 높아진다.

maven에서의 설정방법

pom.xml 의존성 추가

    <dependencies>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>
    </dependencies>
  • 의존성을 위와 같이 추가한다.

pom.xml 빌드 플러그인 추가

<build>
  <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
  • 이렇게 설정한뒤 mvn compile 을 실행시키자. 그럼 @Entity를 붙인 클래스들이 Q타입으로 새롭게 생성된다. ex) Account -> QAccount
    • target 디렉토리 밑에 Q타입의 class가 생성된 걸 볼 수 있다.

사용하기.

  • 먼저 Entity 클래스들을 만들어보자.
  • Member 클래스와 Team 클래스를 만들 것이다.
  • 연관관계는 다 대 일이다. 양방향으로 건다. 연관관계의 주인 즉, fk를 갖고있는 건 '다' 쪽인 Member 클래스다.

Member.class

/**
 * Created by GraceLove
 * Github  : https://github.com/gracelove91
 * Blog    : https://gracelove91.tistory.com
 * Email   : govlmo91@gmail.com
 *
 * @author : Eunmo Hong
 * @since : 2020/06/07
 */

@ToString(of = {"id", "username", "age"})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;


    public Member(String username) {
        this(username, 0, null);
    }


    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

Team.class

/**
 * Created by GraceLove
 * Github  : https://github.com/gracelove91
 * Blog    : https://gracelove91.tistory.com
 * Email   : govlmo91@gmail.com
 *
 * @author : Eunmo Hong
 * @since : 2020/06/07
 */

@ToString(of = {"id", "name"})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

사용 코드

  • 먼저 EntityManager와 JPAQueryFactory를 필드로 두고, EntityManager는 빈을 주입받자.

  • 그 다음 JPAQueryFactory의 인스턴스를 생성하는데, 생성자 인자로 EntityManager를 주자.

    • JPAQueryFactory는 스레드 세이프 하기 때문에 빈으로 등록시켜서 싱글톤으로 이용해도 괜찮다.
  • 테스트를 해볼 데이터를 @BeforeEach 로 각 테스트 이전에 넣어준다.

    @SpringBootTest
    @Transactional
    class MemberTest {
    
      @PersistenceContext //@Autowired
      EntityManager em;
      JPAQueryFactory queryFactory;
    
      @BeforeEach
      public void testEntity() {
          queryFactory = new JPAQueryFactory(em);
    
          Team teamA = new Team("teamA");
          Team teamB = new Team("teamB");
          em.persist(teamA);
          em.persist(teamB);
    
          Member member1 = new Member("member1", 10, teamA);
          Member member2 = new Member("member2", 20, teamA);
    
          Member member3 = new Member("member3", 30, teamB);
          Member member4 = new Member("member4", 40, teamB);
          em.persist(member1);
          em.persist(member2);
          em.persist(member3);
          em.persist(member4);
      }
      ...
    }
  • 이렇게 한다면 디비에는 다음과 같이 값이 들어갈 것이다.

전체 소스 : https://github.com/gracelove91/querydsl-practice/blob/master/src/test/java/kr/gracelove/querydsl/entity/MemberTest.java

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

[JPA]open-session-in-view 를 알아보자  (1) 2020.07.01
@EntityGraph  (0) 2020.02.11
순수 JPA레포지토리의 페이징과 스프링 데이터 JPA레포지토리의 페이징.  (0) 2020.02.10
연관관계의 주인  (0) 2020.01.27
준영속 상태  (0) 2020.01.16

Open-In-View 또는 Open-Session-In-View 또는 Open-EntityManager-In-View 란?

  • 관례상 OSIV 라고 한다.
  • true일 경우 영속성 컨텍스트가 트랜잭션 범위를 넘어선 레이어까지 살아있다.
    • Api라면 클라이언트에게 응답될 때까지, View라면 View가 렌더링될 때까지 영속성컨텍스트가 살아있다.
  • false일 경우 트랜잭션을 종료할 때 영속성 컨텍스트 또한 닫힌다.
  • 기본값은 true 이다

코드

간단히 코드로 false와 true일 때 어떤 차이가 있는지 살펴보자.

  • Member.class
/**
 * @author : Eunmo Hong
 * @since : 2020/06/30
 */

@ToString(of = "name")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    @Builder
    public Member(String name, Team team) {
        this.name = name;
        this.team = team;
    }


}
  • Team.class
/**
 * @author : Eunmo Hong
 * @since : 2020/06/30
 */

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

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;
    private String teamName;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String teamName) {
        this.teamName = teamName;
    }
}

Member와 Team은 다 대 일 관계이며, 서로 양방향 연관관계를 맺었다.
Lazy 로딩이기 때문에 Member를 찾게된다면 Member 안의 Team은 프록시객체가 된다.
Team의 프로퍼티를 건드려야 비로소 Team 객체가 로딩된다.

  • Inin.class
/**
 * @author : Eunmo Hong
 * @since : 2020/07/01
 */

@RequiredArgsConstructor
@Component
public class Init implements ApplicationRunner {
    private final MemberRepository memberRepository;
    private final TeamRepository teamRepository;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Team teamA = teamRepository.save(new Team("teamA"));
        memberRepository.save(Member.builder()
                .name("member1")
                .team(teamA)
                .build());
    }
}

어플리케이션이 구동되면 실행되는 코드다.
이름이 "teamA" 인 Team 객체를 save 한다.
이름이 "member1" 이며 앞서 save한 Team에 소속된 Member를 Save 한다.
그렇다면 DB 상황은 다음과 같다.

  • MemberController.class
/**
 * @author : Eunmo Hong
 * @since : 2020/06/30
 */

@RequiredArgsConstructor
@RestController
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/members/{name}")
    public MemberDto findMember(@PathVariable String name) {
        Member member = memberService.findByName(name);
        return MemberDto.builder()
                .name(member.getName())
                .teamName(member.getTeam().getTeamName())
                .build();
    }
}

간단한 컨트롤러다.
Query String 으로 들어온 String name 으로 해당하는 Member를 찾고,
MemberDto에 Member의 이름과, Member가 속한 Team의 이름을 Set 해주고 JSON으로 반환한다.
화면엔 이렇게 뜬다.

Controller의 return ...teamName(member.getTeam().getTeamName()) 코드에서
Proxy 객체인 Team의 프로퍼티인 TeamName 을 건드렸기 때문에 Lazy loading 이 됐다. 따라서 결과도 잘 나온다.

하지만 OSIVfalse 로 설정한다면?

  • application.yml
    spring:
    datasource:
      url: jdbc:h2:tcp://localhost/~/jpa-sample
      username: sa
      password:
    jpa:
      open-in-view: false

이와 같이 org.hibernate.LazyInitializationException: could not initialize proxy [kr.gracelove.osivdemo.domain.Team#1] - no Session 이라는 익셉션과 함께 500 에러가 뜨는 걸 볼 수 있다.

이는 영속성컨텍스트가 Transaction 범위 바깥인 Controller에서 Lazy loading 을 시도했기 때문이다.
영속성 컨텍스트가 닫혔다면 Lazy loading 또한 할 수 없다!

정리

  • OSIV가 true
    • 사용자에게 응답 또는 view가 렌더링 될 때까지 영속성컨텍스트를 유지한다.
  • OSIV가 false
    • Transaction이 끝나면 영속성컨텍스트 또한 닫힌다.

너무나도 당연하게 OSIV를 true로 두는 게 좋아보이지만! 단점이 존재한다.
영속성 컨텍스트를 유지한다는 건, DB Connection 또한 계속 가지고 있다는 뜻이다.
실시간 트래픽이 중요한 어플리케이션에서는 DB Connection이 모자를 수 있다. 성능이 중요하다면 OSIV는 false로 설정하자.

전체 코드는 다음 링크에서 볼 수 있다.

https://github.com/gracelove91/jpa-practice/tree/master/osiv-demo

레퍼런스

실전! 스프링 부트와 JPA활용 2편 - 김영한님
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94/dashboard

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

[QueryDSL] QueryDSL 사용법  (1) 2020.07.07
@EntityGraph  (0) 2020.02.11
순수 JPA레포지토리의 페이징과 스프링 데이터 JPA레포지토리의 페이징.  (0) 2020.02.10
연관관계의 주인  (0) 2020.01.27
준영속 상태  (0) 2020.01.16

개요

  • HashTable 은 Key - Value 로 이루어진 자료구조다.
  • HashTable.get(Object key)를 쓰면 해당 key와 맞는 value를 반환한다.
  • 이름에서 알 수 있듯이 HashCode를 이용한다.
  • Key를 HashFunction를 통해 정수 Hash를 만들어내고, 그 Hash를 이용해 데이터에 접근하기 때문에 속도가 매우 빠르다.( O(1) )
    • 정수 Hash를 통해 데이터에 접근해서 속도가 빠르다는데.. 그렇다면 동일한 Hash를 반환해서 동일 인덱스에 데이터가 중복될 수 있다. 이를 Hash Colliision(해시 충돌)이라 부른다. 최악의 경우 데이터 검색에 O(n) 이 걸린다. 때문에 Hash를 만들어내는 HashFunction에 쓰이는 Hash Algorithm을 잘 만들어야 한다. 입력받은 Key를 Hash로 만들 때, 최대한 잘 분배해야한다

HashTable과 HashMap의 차이는 ?
HashTable은 Thread Safe 하다.
HashMap은 Thread Unsafe 하다.
동작방식은 비슷하다.

Hash Collision이 발생한다면?

  • 해당 인덱스의 배열 저장 위치에다 바로 데이터를 집어넣는 게 아니라, LinkedList를 이용해서 차곡차곡 쌓는 방법을 쓸 수 있다.
  • 인덱스 중 비어있는 인덱스를 할당한다.

구현

key와 value를 가지고 있는 Node이너클래스 작성.

private static class Node {
    private String key;
    private String value;

    private Node(String key, String value) {
        this.key = key;
        this.value = value;
    }
}

MyHashTable을 만들고, 위에서 작성한 Node를 제네릭파라미터로 갖는 멤버 LinkedList배열 선언.

public class MyHashTable {
    LinkedList<Node>[] data;

    public MyHashTable(int size) {
        this.data = new LinkedList[size];
    }
}

HashCode를 생성해내는 getHashCode(String key) 메서드 작성

private int getHashCode(String key) {
    int hashcode = 0;
    for(char c : key.toCharArray()) {
        hashcode += c;
    }
    return hashcode;
}
  • 간단한 해쉬 알고리즘이다. key 문자열의 각각 문자를 아스키코드로 만들어 모두 더한다.
  • 맘에 들지 않는다면 Objects.hashcode(Object o) 를 쓰자

hashcode를 위에 작성한 LinkedList배열의 인덱스로 변환하는 메서드 작성

private int convertToIndex(int hashcode) {
    return hashcode % data.length;
}
  • 역시 간단한 로직이다. hashcode를 위에서 선언한 LinkedList 배열의 길이로 나머지 연산한 결과다.

입력받은 key로 LinkedList에서 Node를 찾는 searchKey 메서드 작성

private Node searchKey(LinkedList<Node> list, String key) {
    if(list == null) return null;
    for (Node node : list) {
        if (node.key.equals(key)) {
            return node;
        }
    }
    return null;
}

데이터를 집어넣는 public api인 put 메서드 작성

public void put(String key, String value) {
    int hashCode = getHashCode(key);
    int index = convertToIndex(hashCode);
    LinkedList<Node> list = data[index];
    if (list == null) {
        list = new LinkedList<Node>();
        data[index] = list;
    }

    Node node = searchKey(list, key);
    if (node == null) {
        list.addLast(new Node(key, value));
    }else {
        node.value = value;
    }
}
  1. key를 hashcode로 변환한다.
  2. (1)에서 변환한 hashcode를 index로 변환한다.
  3. 위에서 선언한 LinkedList 배열에서 index로 LinkedList를 찾는다.
    • 만약 null 이라면 새로운 LinkedList를 만들고 해당index의 배열에 할당한다.
  4. searchKey 메서드로 (3)에서 찾은 LinkedList와 key를 파라미터로 넘겨주면 Node가 나온다.
    • Node가 null 이라면 key에 해당하는 value가 없다는 뜻이므로 LinkedList의 마지막 위치에 Node를 만들어 저장한다.
    • null이 아니라면 기존에 있는 value를 바꿔치기한다.

데이터를 꺼내오는 public api인 get 메서드 작성

public String get(String key) {
    int hashCode = getHashCode(key);
    int index = convertToIndex(hashCode);
    LinkedList<Node> list = data[index];
    Node node = searchKey(list, key);
    return node == null ? null : node.value;
}
  1. key를 hashcode로 변환한다.
  2. (1)에서 변환한 hashcode를 index로 변환한다.
  3. searchKey 메서드로 해당 key를 갖고있는 Node를 찾는다.
  4. Node가 null 이라면 key에 해당하는 값이 없다는 뜻이므로 null을 반환하고, null이 아니라면 key에 해당하는 값이 있다는 뜻이므로 value를 return 한다.

전체 코드

/**
 * @author : Eunmo Hong
 * @since : 2020/06/27
 */

public class MyHashTable {

    LinkedList<Node>[] data;

    public MyHashTable(int size) {
        this.data = new LinkedList[size];
    }

    public void put(String key, String value) {
        int hashCode = getHashCode(key);
        int index = convertToIndex(hashCode);
        LinkedList<Node> list = data[index];
        if (list == null) {
            list = new LinkedList<Node>();
            data[index] = list;
        }

        Node node = searchKey(list, key);
        if (node == null) {
            list.addLast(new Node(key, value));
        }else {
            node.value = value;
        }
    }

    public String get(String key) {
        int hashCode = getHashCode(key);
        int index = convertToIndex(hashCode);
        LinkedList<Node> list = data[index];
        Node node = searchKey(list, key);
        return node == null ? null : node.value;
    }

    private int getHashCode(String key) {
        int hashcode = 0;
        for (char c : key.toCharArray()) {
            hashcode += c;
        }
        return hashcode;
    }

    private int convertToIndex(int hashCode) {
        return hashCode % data.length;
    }

    private Node searchKey(LinkedList<Node> list, String key) {
        if(list == null) return null;
        for (Node node : list) {
            if (node.key.equals(key)) {
                return node;
            }
        }
        return null;
    }



    private static class Node {
        private String key;
        private String value;

        private Node(String key, String value) {
            this.key = key;
            this.value = value;
        }
    }
}

실행

/**
 * @author : Eunmo Hong
 * @since : 2020/06/27
 */

public class HashSample {
    public static void main(String[] args) {
        MyHashTable map = new MyHashTable(3);
        map.put("hong" , "Eunmo Hong");
        map.put("grace" , "GraceLove");
        map.put("github", "https://github.com/gracelove91");
        map.put("tistory", "https://gracelove91.tistory.com");
        map.put("email", "govlmo91@gmail.com");
    }
}
  • map.put("hong" , "Eunmo Hong");
    1. 'h(104)', 'o(111)', 'n(110)', 'g(103)' 의 각각 아스키코드 값을 모두 더한다. (총합 428)
      • private int getHashCode(String key)
    2. hashCode와 위에서 선언한 LinkedList 배열의 length를 나머지 연산을 한다. (428 % 3 = 2)
      • private int convertToIndex(int hashcode)
    3. LinkedList[2]에 위치한 LinkedList를 찾아온다.
      • public void put(String key, String value) {
          ...
          LinkedList<Node> list = data[index];
          ...
        }
    4. (3)에서 찾은 LinkedList는 null이니 새로운 LinkedList를 만들어서 (3)에서 찾는 LinkedList 변수에 할당한다.
      • public void put(String key, String value) {
          ...
          if(list == null) {
           list = new LinkedList<>();
           data[index] = list;
        }
        ...
        }
    5. searchKey로 key와 (4)에서 할당한 LinkedList를 넘겨주면 Node를 반환한다.
      • public void put(String key, String value) {
        ...
          Node node = searchKey(list, key);
        ...
        }
    6. Node가 null이므로 key와 value로 새로운 Node를 만들어서 (3)에서 찾은 LinkedList의 마지막 위치에 할당해준다
      • public void put(String key, String value) {
        ...
            if (node == null) {
             list.addLast(new Node(key, value));
          }
        ...
        }

개요

핵심 로직을 건들지 않고, 부가적인 작업을 수행할 때 주로 사용되는 패턴이다.
예를 들어 Event가 생성될 때, 만들어진 시간을 콘솔에 기록하고 싶어졌다!
여기서 핵심 로직은 Event 를 생성하는 로직, 부가적인 로직은 만들어진 시간을 콘솔에 기록하는 코드다.

사용방법은 이렇다.

  1. 핵심로직을 수행하는 클래스와 부가적인 로직을 수행하는 클래스(프록시) 모두 같은 인터페이스를 참조하게끔한다.
  2. 클라이언트 코드에선 해당 인터페이스 타입을 사용하되, 실제 타입은 프록시 객체로 만든다.
  3. 프록시객체는 부가적인 로직을 수행한 뒤, 자신이 참조하고있는 핵심로직을 수행하는 객체를 사용한다.

구현

도메인 객체

public class Event {
    private String name;

    ...constructor..getter..toString...
}

부가적인 로직을 수행하는 프록시와, 핵심 로직을 수행하는 서브젝트 모두가 구현하고 있는 인터페이스.

public interface EventService {

    void createEvent(Event event);
}

부가적인 로직을 수행하는 프록시

public class EventServiceProxy implements EventService{

    private EventService eventService;

    public EventServiceProxy(EventService eventService) {
        this.eventService = eventService;
    }

    @Override
    public void createEvent(Event event) {
        System.out.println("만들어진 시간 : " + LocalDateTime.now());
        eventService.createEvent(event);
    }
}

핵심 로직을 수행하는 서브젝트

public class EventServiceImpl implements EventService{

    @Override
    public void createEvent(Event event) {
        System.out.println("이벤트가 만들어졌습니다 : " + event);
    }
}

클라이언트

class EventServiceTest {

    @Test
    void proxy() {
        EventService eventService = new EventServiceProxy(new EventServiceImpl());
        eventService.createEvent(new Event("hmmmmmmmmteresting"));
    }

}

콘솔에 기록되는 내용

만들어진 시간 : 2020-06-24T01:33:04.665159
이벤트가 만들어졌습니다 : Event{eventName='hmmmmmmmmteresting'}

레퍼런스

https://www.inflearn.com/course/the-java-code-manipulation/dashboard

'JAVA' 카테고리의 다른 글

1022 템플릿메서드  (0) 2019.10.22
[JUnit5] gradle에 junit5 끼얹기  (0) 2019.08.11

개요.

스프링부트 2.3 버전이 릴리즈되면서 부트에서 도커 이미지를 만들 수 있도록 지원한다!
본 포스팅은 부트로 도커이미지를 빌드하는 법을 설명한다. (인텔리제이 기준)
도커의 기본적인 이해는 다른 블로그를 참고하길 바란다.

프로젝트 생성

간단하게 Spring Web, Spring Boot DevTools 을 의존성에 추가시키고 프로젝트를 만들자.

Spring boot DevTools

간단히 말하자면 classpath에 속해있는 파일들의 변경사항이 생기면 자동으로 어플리케이션을 재시작한다. 재시작버튼이 아니라 build버튼을 눌러도 같은 효과를 기대할 수 있다.

코드작성

SampleController.class

간단한 controller 클래스를 만들자!

package kr.gracelove.builddockerimagedemo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by GraceLove on 2020/06/02.
 * Contact : govlmo91@gmail.com
 */
@RestController
public class SampleController {

    @GetMapping("/")
    public String hello() {
        return "Hello World";
    }
}

실행

GET 방식으로 '/' 로 들어온다면 응답본문에 "Hello World" 를 반환하는 간단한 컨트롤러다. 의도한대로 잘 작동하는 지 애플리케이션을 시동하고, '/'로 접속해보자.

의도한 대로 동작하는 걸 볼 수 있다.

DevTools를 이용한 재시작

hello() 메서드가 return 하는 문자열을 변경한 뒤 build 버튼을 눌러보자. 그 다음 다시 '/' 경로로 접속해보자.

변경 전 return 하는 문자열 : "Hello World"
변경 후 return 하는 문자열 : "Hello GraceLove"

변경 후 다시 build를 한 뒤 '/'로 접속해보자.

어플리케이션을 재시작하지 않아도 변경사항이 적용됨을 확인할 수 있다!

도커이미지 빌드하기.

어플리케이션을 중지시킨 뒤, 터미널에서 다음과 같은 명령어를 내리자!
./mvnw spring-boot:build-image

만약 도커이미지를 처음 build 하는 거라면 시간이 굉장히 오래걸리니 인내심을 갖자.

 

빌드가 완료됐다!

도커 컨테이너 실행시키기.

다음과 같은 명령어를 입력하자!

docker run --tty --publish 8080:8080 <위에서 build된 이미지 이름>
docker run --tty --publish 8080:8080 build-docker-image-demo:0.0.1-SNAPSHOT

옵션은 다른 블로그나 책을 참고하자.

실행이 됐다. 다시 '/' 로 접속해보자 브라우저에 Hello GraceLove 가 표시되는 걸 볼 수 있다.

도커로 실행시킨 어플리케이션 remote로 접속하고 DevTools로 재시작해보기.

먼저 Run/Debug Configurations 로 들어가자. Add New Configuration 으로 Application을 추가한다.

추가된 Application에서는 다음과 같이 설정한다.

Main class :
org.springframework.boot.devtools.RemoteSpringApplication

Program arguments :
http://localhost:8080

완료됐으면 추가한 Remote Application을 실행시킨 후 '/'로 접속해보자. 우리가 기대한 대로 잘 동작하는 것을 볼 수 있다.

DevTools 사용해보기

지금 상황에서는 hello() 메서드의 return 값을 수정한 뒤 build해도 재시작이 되지 않는다. 원격 어플리케이션에 devtools 를 사용하기 위해선 다음과 같은 작업이 필요하다.

pom.xml

...
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludeDevtools>false</excludeDevtools>
                </configuration>
            </plugin>
        </plugins>
    </build>

실행.

pom.xml 을 위와 같이 설정한 뒤 다시 도커 이미지를 빌드하자. ./mvnw spring-boot:build-image
그 다음 도커를 실행시킨 뒤, docker run --tty --publish 8080:8080 build-docker-image-demo:0.0.1-SNAPSHOT
Remote Application을 실행시켜보자.

실행시킨뒤 '/'으로 가서 Hello GraceLove가 브라우저에 표시되는 지 보고, hello()메서드의 return 문자열을 'Hello World' 로 수정한 뒤 build해보자.
다음과 같은 로그가 뜬다.

다시 '/' 로 가보면 우리가 기대했던 'Hello World'가 표시되는 걸 볼 수 있다.
이로써 Remote Application 에도 DevTools가 잘 붙는 걸 알 수 있다.

+ Recent posts