<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>티끌모아 산을 쌓아보자</title>
    <link>https://private-space.tistory.com/</link>
    <description>https://github.com/dhmin5693
dhmin5693@naver.com</description>
    <language>ko</language>
    <pubDate>Wed, 13 May 2026 03:19:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>감동이중요해</managingEditor>
    <item>
      <title>JPA 사용 시 주의할 점</title>
      <link>https://private-space.tistory.com/127</link>
      <description>&lt;h1&gt;JPA 사용 시 주의할 점&lt;/h1&gt;
&lt;h2&gt;영속 상태 보장&lt;/h2&gt;
&lt;p&gt;JPA Entity는 영속상태인가, 아닌가에 따라서 동작 상 제한사항이 있을 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들면,  영속 상태의 entity는 delete가 가능하지만 비영속 상태의 entity는 불가능하다.&lt;/p&gt;
&lt;p&gt;특정 로직을 수행하는 경우 영속 상태를 인지하고 있어야한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public void deleteQuestion(User loginUser, Question question) throws CannotDeleteException {

    if (question.isDeleted()) {
        throw new CannotDeleteException(&amp;quot;이미 삭제된 질문입니다.&amp;quot;);
    }

    if (!question.isOwner(loginUser)) {
        throw new CannotDeleteException(&amp;quot;질문을 삭제할 권한이 없습니다.&amp;quot;);
    }

    List&amp;lt;Answer&amp;gt; answers = question.getAnswers();
    for (Answer answer : answers) {
        if (!answer.isOwner(loginUser)) {
            throw new CannotDeleteException(&amp;quot;다른 사람이 쓴 답변이 있어 삭제할 수 없습니다.&amp;quot;);

    List&amp;lt;DeleteHistory&amp;gt; deleteHistories = new ArrayList&amp;lt;&amp;gt;();
    question.setDeleted(true);
    deleteHistories.add(new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now()));
    for (Answer answer : answers) {
        answer.setDeleted(true);
        deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now()));
    }
    deleteHistoryService.saveAll(deleteHistories);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드는 질문에 쓰여진 답변을 삭제하는 서비스 메소드이다.&lt;/p&gt;
&lt;p&gt;메소드 명은 &lt;code&gt;deleteQuestion&lt;/code&gt; 이며 매개변수로 &lt;code&gt;Question&lt;/code&gt; 을 넘겨주고 있다.&lt;/p&gt;
&lt;p&gt;entity를 삭제하기 위해서는 넘겨받는 &lt;code&gt;Question&lt;/code&gt; 이 반드시 영속 상태여야 하지만&lt;/p&gt;
&lt;p&gt;코드 사용자 측에서는 실수로 비영속 상태의 &lt;code&gt;Question&lt;/code&gt; 을 넘겨 버릴 가능성이 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Question&lt;/code&gt; 대신 &lt;code&gt;question_id&lt;/code&gt;를 넘기도록 수정하면 영속 상태를 보장할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public void deleteQuestion(long loginUserId, long questionId) throws CannotDeleteException {

    Question question =
        questionRepository.findById(questionId)
                            .orElseThrow(() -&amp;gt; new CannotDeleteException(&amp;quot;질문을 찾을 수 없습니다.&amp;quot;));

    if (question.isDeleted()) {
        throw new CannotDeleteException(&amp;quot;이미 삭제된 질문입니다.&amp;quot;);
    }

    User loginUser =
        userRepository.findById(loginUserId)
                        .orElseThrow(() -&amp;gt; new NotFoundException(&amp;quot;사용자를 찾을 수 없습니다.&amp;quot;));

    if (!question.isOwner(loginUser)) {
        throw new CannotDeleteException(&amp;quot;질문을 삭제할 권한이 없습니다.&amp;quot;);
    }

    List&amp;lt;Answer&amp;gt; answers = question.getAnswers();
    for (Answer answer : answers) {
        if (!answer.isOwner(loginUser)) {
            throw new CannotDeleteException(&amp;quot;다른 사람이 쓴 답변이 있어 삭제할 수 없습니다.&amp;quot;);
        }
    }

    List&amp;lt;DeleteHistory&amp;gt; deleteHistories = new ArrayList&amp;lt;&amp;gt;();
    question.delete();
    deleteHistories.add(new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now()));
    for (Answer answer : answers) {
        question.deleteAnswer(answer);
        deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now()));
    }
    deleteHistoryService.saveAll(deleteHistories);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;의존성 사이클&lt;/h2&gt;
&lt;h3&gt;의존성 세미나 자료&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/dJ5C4qRqAgA&quot;&gt;https://youtu.be/dJ5C4qRqAgA&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.slideshare.net/baejjae93/ss-151545329&quot;&gt;[수정본] 우아한 객체지향&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;의존성 사이클에 관한 문제는 JPA만의 문제점은 아니다.&lt;/p&gt;
&lt;p&gt;다만 JPA에서 다대일 관계를 표현하면 양방향으로 의존하는 소스 코드가 간혹 나타나기 때문에 함께 적었다.&lt;/p&gt;
&lt;p&gt;객체 참조를 통해 더 객체지향적인 코드를 작성할 수 있지만 결합도가 크게 상승하여 문제가 될 수 있다.&lt;/p&gt;
&lt;p&gt;질문과 답변의 예시를 살펴볼건데, 의존성은 아래처럼 순환하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Answer&lt;/code&gt; → &lt;code&gt;Question&lt;/code&gt; → &lt;code&gt;QuestionAnswers&lt;/code&gt; → &lt;code&gt;Answer&lt;/code&gt; → ...&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;QuestionAnswers&lt;/code&gt; 는 &lt;code&gt;List&amp;lt;Answer&amp;gt;&lt;/code&gt; 를 일급 콜렉션으로 포장한 클래스이다.&lt;/p&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Entity
public class Answer extends BaseEntity implements Serializable {

    private static final long serialVersionUID = 951549577740790162L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &amp;quot;writer_id&amp;quot;, foreignKey = @ForeignKey(name = &amp;quot;fk_answer_writer&amp;quot;))
    private User writer;

    @ManyToOne
    @JoinColumn(name = &amp;quot;question_id&amp;quot;, foreignKey = @ForeignKey(name = &amp;quot;fk_answer_to_question&amp;quot;))
    private Question question;

    @Column(columnDefinition = &amp;quot;LONGTEXT&amp;quot;)
    private String contents;

    @Column
    private boolean deleted = false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Entity
public class Question extends BaseEntity implements Serializable {

    private static final long serialVersionUID = -5316964078122252034L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    private Long id;

    @Column(length = 100)
    private String title;

    @Column(columnDefinition = &amp;quot;LONGTEXT&amp;quot;)
    private String contents;

    @ManyToOne
    @JoinColumn(name = &amp;quot;writer_id&amp;quot;, foreignKey = @ForeignKey(name = &amp;quot;fk_question_writer&amp;quot;))
    private User writer;

    @Embedded
    private QuestionAnswers questionAnswers;

    @Column
    private boolean deleted = false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Embeddable
public class QuestionAnswers implements Serializable {

    private static final long serialVersionUID = 8457250053092405727L;

    @OneToMany(mappedBy = &amp;quot;question&amp;quot;)
    @Where(clause = &amp;quot;deleted = 0&amp;quot;)
    private final Set&amp;lt;Answer&amp;gt; answers = new HashSet&amp;lt;&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;사이클 관계를 이루는 구조에서 하나의 클래스만 수정해도 다른 클래스에 전부 영향이 가게 된다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;QuestionAnswers&lt;/code&gt; 의 &lt;code&gt;Set&amp;lt;Answer&amp;gt;&lt;/code&gt; 를 &lt;code&gt;List&amp;lt;Answer&amp;gt;&lt;/code&gt; 로 교체한다고 하면 &lt;code&gt;Answer&lt;/code&gt; 나 &lt;code&gt;Question&lt;/code&gt; 에서 참조하는 부분을 모두 수정해주어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Entity
public class Answer extends BaseEntity implements Serializable {

    private static final long serialVersionUID = 951549577740790162L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &amp;quot;writer_id&amp;quot;, foreignKey = @ForeignKey(name = &amp;quot;fk_answer_writer&amp;quot;))
    private User writer;

    @Column // Question -&amp;gt; question_id
    private Long questionId;

    @Column(columnDefinition = &amp;quot;LONGTEXT&amp;quot;)
    private String contents;

    @Column
    private boolean deleted = false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Answer&lt;/code&gt; 의 필드였던 &lt;code&gt;Question&lt;/code&gt; 을 &lt;code&gt;questionId&lt;/code&gt; 로 바꿔주면서 사이클이 완전히 끊겼다.&lt;/p&gt;
&lt;p&gt;필드 참조를 통해 얻어갈 수 있던 객체지향적인 장점은 잃게 되지만 결합도를 낮춰 코드 수정 위험도를 줄일 수 있다.&lt;/p&gt;</description>
      <category>Java/JPA</category>
      <category>Entity</category>
      <category>JPA</category>
      <category>영속</category>
      <category>영속상태</category>
      <category>의존성</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/127</guid>
      <comments>https://private-space.tistory.com/127#entry127comment</comments>
      <pubDate>Thu, 22 Jul 2021 01:33:06 +0900</pubDate>
    </item>
    <item>
      <title>ATDD 아주 간단히 찍먹</title>
      <link>https://private-space.tistory.com/126</link>
      <description>&lt;h1&gt;ATDD&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;같이 읽을 만한 자료&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Acceptance_test%E2%80%93driven_development&quot;&gt;https://en.wikipedia.org/wiki/Acceptance_test–driven_development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/articles/practical-test-pyramid.html&quot;&gt;https://martinfowler.com/articles/practical-test-pyramid.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ATDD를 간단하게 소개해보자면...&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;인수 테스트는 고객과 개발자, 그리고 테스터간의 커뮤니케이션을 기반으로 한 개발 방법론이다. 이러한 프로세스는 개발자와 테스터가 구현 전에 고객의 요구를 이해하는 데 도움이 되며 고객이 자신의 도메인 언어로 대화 할 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Trade off of TDD&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;테스트 케이스가 없는 코드는 깨끗한 코드가 아니다. 아무리 코드가 우아해도, 아무리 가독성이 높아도, 테스트 케이스가 없으면 깨끗하지 않다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;테스트는 코드 품질을 일정 수준까지 보장할 수 있는 가장 확실한 수단&lt;/li&gt;
&lt;li&gt;TDD는 개발보다는 테스트를 우선으로 개발하는 방법론&lt;/li&gt;
&lt;li&gt;TDD를 마냥 진행하기에는 몇 가지 어려운 문제점이 있다.&lt;ul&gt;
&lt;li&gt;TDD는 실패한 테스트부터 작성해야 한다. 어떻게 시작하고 어디까지 구현해야 하는가?&lt;/li&gt;
&lt;li&gt;막상 TDD로 구현을 하고 나니 쓰지 않는 코드가 완성되었다.&lt;/li&gt;
&lt;li&gt;어렵사리 완성을 했지만 요구사항이 변경되며 테스트 코드까지 대거 변경해야 한다. 어디부터 다시 TDD를 시작해야 할까?&lt;/li&gt;
&lt;li&gt;TDD로 개발하니 어디부터 구현해야 하는지 감이 잘 오지 않아 생산성이 저하되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ATDD&lt;/code&gt; 는 이 고민에 대한 해결책이 될 수 있다. 인수 테스트는 작업의 시작과 끝이 명확한 테스트이기 때문에 생산성에 큰 도움이 된다.&lt;/p&gt;
&lt;h2&gt;ATDD 개요&lt;/h2&gt;
&lt;h3&gt;ATDD Cycle&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;사용자 시나리오 설계&lt;/li&gt;
&lt;li&gt;작업 대상의 &lt;code&gt;요청값&lt;/code&gt;과 &lt;code&gt;응답값&lt;/code&gt; 설정&lt;/li&gt;
&lt;li&gt;위의 절차를 토대로 &lt;code&gt;ATDD&lt;/code&gt; 작성&lt;ul&gt;
&lt;li&gt;작성된 테스트는 &lt;code&gt;Black box&lt;/code&gt; 방식이기 때문에 실제로 어떻게 구현하느냐는 고려 대상이 아니다.&lt;/li&gt;
&lt;li&gt;요청값과 응답값만을 확인하여 전 구간을 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;작성된 &lt;code&gt;ATDD&lt;/code&gt;를 망가뜨리지 않는 선에서 기능과 단위 테스트를 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;인수 테스트의 조건&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;인수 조건이 명확해야 한다.&lt;ul&gt;
&lt;li&gt;인수 조건은 개발자가 소프트웨어의 의도를 명확하게 이해했음을 드러내는 수단이다.&lt;/li&gt;
&lt;li&gt;인수 조건의 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실제 요청/응답 환경과 유사한 테스트 환경이 구축되어 있어야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;ATDD의 장점&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;테스트 의도를 더 명확하게 전달할 수 있어 구현 대상에 대한 이해가 빠름.&lt;/li&gt;
&lt;li&gt;작업의 시작과 끝부터 정한 뒤 Top-down 방식으로 코드를 작성하는데 도움이 됨.&lt;/li&gt;
&lt;li&gt;테스트를 최대한 수정하지 않으면서 리팩토링하기에 수월함.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;ATDD 도구 소개&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;302&quot; data-origin-height=&quot;44&quot; data-filename=&quot;Untitled.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xCKz9/btq94qvUtwp/l9HadiBtQY0rl2y0kWneM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xCKz9/btq94qvUtwp/l9HadiBtQY0rl2y0kWneM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xCKz9/btq94qvUtwp/l9HadiBtQY0rl2y0kWneM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxCKz9%2Fbtq94qvUtwp%2Fl9HadiBtQY0rl2y0kWneM1%2Fimg.png&quot; data-origin-width=&quot;302&quot; data-origin-height=&quot;44&quot; data-filename=&quot;Untitled.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;특징&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;이해하기 쉬운 구조로 단순화된 API&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt; given().
     param(&amp;quot;key1&amp;quot;, &amp;quot;value1&amp;quot;).
     param(&amp;quot;key2&amp;quot;, &amp;quot;value2&amp;quot;).
 when().
     post(&amp;quot;/somewhere&amp;quot;).
 then().
     body(containsString(&amp;quot;OK&amp;quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;검증을 위한 다양한 방식의 API 지원&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt; // Example with JsonPath
 String json = get(&amp;quot;/lotto&amp;quot;).asString();
 List&amp;lt;String&amp;gt; winnerIds = from(json).get(&amp;quot;lotto.winners.winnerId&amp;quot;);

 // Example with XmlPath
 String xml = post(&amp;quot;/shopping&amp;quot;).andReturn().body().asString();
 Node category = from(xml).get(&amp;quot;shopping.category[0]&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;프로젝트에 추가하기&lt;/h3&gt;
&lt;p&gt;gradle 기준으로 작성&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;rest-assured&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies { 
    testImplementation &amp;#39;io.rest-assured:rest-assured:3.3.0&amp;#39; 
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;spring-restdocs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies { 
    testImplementation &amp;#39;org.springframework.restdocs:spring-restdocs-restassured&amp;#39; 
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;프로젝트에 더 맞는 의존성을 불러와서 사용하면 된다.&lt;/p&gt;
&lt;h3&gt;예제 코드&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dhmin5693/ATDD-product-example&quot;&gt;dhmin5693/ATDD-product-example&lt;/a&gt;&lt;/p&gt;</description>
      <category>Java</category>
      <category>atdd</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/126</guid>
      <comments>https://private-space.tistory.com/126#entry126comment</comments>
      <pubDate>Wed, 21 Jul 2021 01:37:11 +0900</pubDate>
    </item>
    <item>
      <title>AWS 망(VPC) 구성하기 (2) - 네트워크 구성</title>
      <link>https://private-space.tistory.com/125</link>
      <description>&lt;h1&gt;AWS 네트워크 구성&lt;/h1&gt;
&lt;h2&gt;VPC (Virtual private cloud)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/what-is-amazon-vpc.html&quot;&gt;공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AWS 상에서 내가 사용할 클라우드 자원의 네트워크를 직접 구성하는 서비스이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;667&quot; data-filename=&quot;1.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rl7Vt/btq9DmnxMEC/VkjmG7hRSEa1tRFC0XCLu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rl7Vt/btq9DmnxMEC/VkjmG7hRSEa1tRFC0XCLu0/img.png&quot; data-alt=&quot;네트워크 망 구성도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rl7Vt/btq9DmnxMEC/VkjmG7hRSEa1tRFC0XCLu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRl7Vt%2Fbtq9DmnxMEC%2FVkjmG7hRSEa1tRFC0XCLu0%2Fimg.png&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;667&quot; data-filename=&quot;1.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네트워크 망 구성도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;518&quot; data-filename=&quot;2.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1wvRF/btq9DvrBLg5/ky5WFSGBhRwvGfu5Zdr2fK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1wvRF/btq9DvrBLg5/ky5WFSGBhRwvGfu5Zdr2fK/img.png&quot; data-alt=&quot;VPC 구성도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1wvRF/btq9DvrBLg5/ky5WFSGBhRwvGfu5Zdr2fK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1wvRF%2Fbtq9DvrBLg5%2Fky5WFSGBhRwvGfu5Zdr2fK%2Fimg.png&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;518&quot; data-filename=&quot;2.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;VPC 구성도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;일반적인 네트워크 망은 수많은 물리적 장비를 직접 설치하고 구성 및 구축까지 해야하지만&lt;/p&gt;
&lt;p&gt;VPC는 클라우드 서비스이므로 물리적 장비를 직접 설치할 필요가 없다.&lt;/p&gt;
&lt;p&gt;망의 논리적인 구조를 웹 콘솔에서 관리하고 네트워크 정책을 설정할 수 있다.&lt;/p&gt;
&lt;p&gt;관련 서비스로는 Subnet, Internet gateway, Route table, Security group 등이 존재한다.&lt;/p&gt;
&lt;h3&gt;VPC 생성&lt;/h3&gt;
&lt;p&gt;AWS의 VPC 메뉴로 접속하여 생성 버튼을 누른다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;840&quot; data-filename=&quot;3.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvlpoU/btq9LpQe24S/7zZ0pN90VArFGLLu225QGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvlpoU/btq9LpQe24S/7zZ0pN90VArFGLLu225QGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvlpoU/btq9LpQe24S/7zZ0pN90VArFGLLu225QGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvlpoU%2Fbtq9LpQe24S%2F7zZ0pN90VArFGLLu225QGK%2Fimg.png&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;840&quot; data-filename=&quot;3.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;적절한 이름을 생성하고 I&lt;strong&gt;P 주소는 CIDR 주소&lt;/strong&gt;로 입력한다.&lt;/p&gt;
&lt;p&gt;미리 정의된 &lt;a href=&quot;https://en.wikipedia.org/wiki/Reserved_IP_addresses&quot;&gt;사설 IP 대역(참고)&lt;/a&gt;을 활용하여 적절한 C 클래스의 주소를 넣어보자.&lt;/p&gt;
&lt;p&gt;C 클래스의 경우 서브넷마스크가 255.255.255.0 이기 때문에 이 VPC 망에서 사용 가능한 호스트는 256개이다.&lt;/p&gt;
&lt;p&gt;(왜 256개인지 이해되지 않는다면 CIDR 표기법을 다시 공부해야 한다)&lt;/p&gt;
&lt;p&gt;참고로 여기에 입력하는 IP 주소는 private 주소가 되며 VPC 망 내에서만 인식이 가능하다.&lt;/p&gt;
&lt;p&gt;public IP 주소는 AWS가 직접 관리하기 때문에 사용자가 컨트롤할 수 없다.&lt;/p&gt;
&lt;p&gt;이번 실습에서는 &lt;code&gt;10.20.30.0/24&lt;/code&gt; 대역을 사용한다.&lt;/p&gt;
&lt;h2&gt;서브넷&lt;/h2&gt;
&lt;p&gt;서브넷마스크와 유사한 서비스이다.&lt;/p&gt;
&lt;p&gt;할당된 네트워크를 더욱 작은 단위로 쪼개어 효율적으로 망을 구성할 수 있도록 돕는다.&lt;/p&gt;
&lt;p&gt;즉, VPC가 하나의 큰 망이면 서브넷은 작은 단위의 망이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;위에서 만든 VPC는 최대 256개의 호스트를 갖는데, 이 안에서 더 세밀하게 망을 구성해본다.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;관리용으로 사용할 Subnet: 32개의 호스트&lt;ul&gt;
&lt;li&gt;bastion 서버로 사용 (아래에서 따로 설명)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;내부망으로 사용할 Subnet: 32개의 호스트&lt;ul&gt;
&lt;li&gt;Database 등 외부에 직접 노출하지 않을 인프라 구성&lt;/li&gt;
&lt;li&gt;VPC 내부에서만 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;외부망으로 사용할 Subnet: 64개의 호스트, 2개 구성&lt;ul&gt;
&lt;li&gt;외부에서 접속 가능한 Application 구성&lt;/li&gt;
&lt;li&gt;외부의 트래픽을 견뎌야 하기 때문에 호스트의 수를 가장 많이 할당(총 128개)&lt;/li&gt;
&lt;li&gt;리전은 서울로 선택&lt;/li&gt;
&lt;li&gt;Availability Zone(가용영역, AZ)은 ap-northeast-2a, 2b, 2c, 2d가 존재&lt;ul&gt;
&lt;li&gt;AZ는 데이터 센터를 의미한다.&lt;/li&gt;
&lt;li&gt;특정 AZ가 장애가 발생해도 서비스를 유지하려면 2개의 subnet을 다른 AZ로 생성하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;2b는 EC2 인스턴스가 생성되지 않는다. 2a와 2c에 1개씩 구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;서브넷 생성&lt;/h3&gt;
&lt;p&gt;서브넷 메뉴로 접속하면 아래와 같은 화면이 나타난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;538&quot; data-filename=&quot;4.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wFP0B/btq9JOv6Bex/IdKp9nticqjAT73OJ2dop1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wFP0B/btq9JOv6Bex/IdKp9nticqjAT73OJ2dop1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wFP0B/btq9JOv6Bex/IdKp9nticqjAT73OJ2dop1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwFP0B%2Fbtq9JOv6Bex%2FIdKp9nticqjAT73OJ2dop1%2Fimg.png&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;538&quot; data-filename=&quot;4.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;방금 생성한 VPC를 선택하면 아래 &lt;code&gt;서브넷 설정&lt;/code&gt; 탭의 내용이 변경된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;680&quot; data-filename=&quot;5.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckyDqo/btq9LpJtNmq/HGQ2ySWSzEnyjCOND7uN5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckyDqo/btq9LpJtNmq/HGQ2ySWSzEnyjCOND7uN5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckyDqo/btq9LpJtNmq/HGQ2ySWSzEnyjCOND7uN5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckyDqo%2Fbtq9LpJtNmq%2FHGQ2ySWSzEnyjCOND7uN5K%2Fimg.png&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;680&quot; data-filename=&quot;5.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;서브넷 이름은 용도에 맞게 적당히 넣어주면 된다.&lt;/li&gt;
&lt;li&gt;가용 영역(AZ)는 한 곳에 집중되지 않게 선택한다. 특히 외부망 2개 서브넷은 꼭 다른 AZ를 선택한다.&lt;/li&gt;
&lt;li&gt;CIDR 블록이 여기서 또 나오는데, IP를 어떻게 구성할지를 먼저 생각해보자.&lt;ol&gt;
&lt;li&gt;0번 호스트부터 관리망 32개, 내부망 32개, 외부망 64개, 외부망 64개 순으로 사용한다고 가정한다.&lt;/li&gt;
&lt;li&gt;사용할 개수는 2의 멱승개로 구성했다. CIDR로 표기하기 위함이다.&lt;/li&gt;
&lt;li&gt;관리망: &lt;code&gt;10.20.30.0/27&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;내부망: &lt;code&gt;10.20.30.32/27&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;외부망1: &lt;code&gt;10.20.30.64/26&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;외부망2: &lt;code&gt;10.20.30.128/26&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;서브넷마스크는 왜 /27, /26일까?&lt;/p&gt;
&lt;p&gt;VPC의 서브넷마스크가 /24 이므로 일단 24보다 큰 숫자 중에서 골라야 한다.&lt;/p&gt;
&lt;p&gt;/27이면 호스트 개수가 5개 비트만큼, /26이면 호스트 개수가 6개 비트만큼 할당된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;32 - 27 = 5 ⇒ $2^5$ = 32개&lt;/li&gt;
&lt;li&gt;32 - 26 = 6 ⇒ $2^6$ = 64개&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;32개, 64개 호스트 주소를 할당했지만 실제로 그만큼 사용할 수는 없다.&lt;/p&gt;
&lt;p&gt;몇 개의 호스트의 쓰임새는 이미 예약되어 있기 때문이다. (&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/VPC_Subnets.html&quot;&gt;이 문서에서 확인&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Bastion 서버&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Bastion [ˈbæstiən]. &amp;quot;요새&amp;quot;, &amp;quot;보루&amp;quot;를 뜻하는 영어 단어&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;오버워치에서 자리만 잘 잡으면 말도 안되는 화력을 보여주었던 이 녀석하고 스펠링이 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot; data-filename=&quot;6.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uvNXQ/btq9DlPMMtI/bvIXuwkg2Nw2QF8KEZWjB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uvNXQ/btq9DlPMMtI/bvIXuwkg2Nw2QF8KEZWjB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uvNXQ/btq9DlPMMtI/bvIXuwkg2Nw2QF8KEZWjB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuvNXQ%2Fbtq9DlPMMtI%2FbvIXuwkg2Nw2QF8KEZWjB0%2Fimg.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot; data-filename=&quot;6.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;bastion 서버는 외부의 공격으로부터 서버를 효과적으로 방어하기 위한 수단이다.&lt;/p&gt;
&lt;p&gt;외부에서 EC2에 접속하기 위해서는 일반적으로 SSH를 사용하는데,&lt;/p&gt;
&lt;p&gt;bastion에만 외부 SSH 접속을 허용하고 그 외 모든 서버는 bastion을 통해서만 접속할 수 있게 통제한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장점&lt;ul&gt;
&lt;li&gt;서비스 트래픽과 관리자용 트래픽 구분 가능&lt;/li&gt;
&lt;li&gt;bastion에만 보안을 집중하면 되기 때문에 효율적&lt;/li&gt;
&lt;li&gt;bastion이 공격으로 인해 피해를 보면 bastion만 재구성하면 되므로 서비스 영향을 최소화할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Internet gateway&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/VPC_Internet_Gateway.html&quot;&gt;공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;VPC에 속한 클라우드 자원이 외부 인터넷과 통신할 수 있게 만들어주는 서비스이다.&lt;/p&gt;
&lt;p&gt;생성하고 VPC를 선택하면 끝이다.&lt;/p&gt;
&lt;h2&gt;Route table&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/VPC_Route_Tables.html&quot;&gt;공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;라우터와 비슷한 역할을 하는 서비스이다.&lt;/p&gt;
&lt;p&gt;라우터는 네트워크 계층에 속하는 장비로, 서로 다른 네트워크 간의 통신을 중계한다.&lt;/p&gt;
&lt;p&gt;목적지 정보(MAC 주소)를 테이블에 가지고 있다면 forwading 시켜주고 아니면 drop한다.&lt;/p&gt;
&lt;h3&gt;Route table 생성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;생성한 VPC를 선택하고 용도에 맞는 적당한 이름을 기입하여 Route table을 생성&lt;/li&gt;
&lt;li&gt;관리망, 내부망, 외부망 전용 총 3개를 만들어도 되고, 인터넷 접근 가능 1개/불가능 1개를 만들어도 된다.&lt;/li&gt;
&lt;li&gt;생성된 Route table 정보로 들어가면 라우팅 정보와 연결할 서브넷을 편집할 수 있다.&lt;ul&gt;
&lt;li&gt;VPC 내부망끼리는 모두 통신이 가능해야 한다. &lt;code&gt;10.20.30.0/24&lt;/code&gt; 는 모든 라우팅에 추가한다.&lt;/li&gt;
&lt;li&gt;인터넷에 접근하려면 위에서 생성한 Internet gateway를 라우팅에 추가한다.&lt;ul&gt;
&lt;li&gt;외부망은 외부에서 application에 접근해야 하기 때문에 인터넷 통신이 필요하다.&lt;/li&gt;
&lt;li&gt;관리망은 외부에서 SSH를 통해 관리 목적으로 접근해야 하기 때문에 인터넷 통신이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Security group&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/VPC_SecurityGroups.html&quot;&gt;공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AWS 클라우드 자원의 인바운드 및 아웃바운드 정책을 관리하는 방화벽 서비스이다.&lt;/p&gt;
&lt;h2&gt;Security group 생성&lt;/h2&gt;
&lt;p&gt;관리망, 외부망, 내부망 순으로 보안 그룹을 생성한다.&lt;/p&gt;
&lt;p&gt;순서는 바뀌어도 상관이 없으나, 허용 대상을 보안 그룹으로 선택할 수 있기 때문에&lt;/p&gt;
&lt;p&gt;순서를 맞춰서 만들면 왔다갔다 하는 귀찮음을 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;각 망의 쓰임새에 맞는 인바운드 포트만 개방하고 아웃바운드는 건드리지 않는다.&lt;/p&gt;
&lt;p&gt;또한 보안을 위해 &lt;code&gt;내 IP&lt;/code&gt; 혹은 &lt;code&gt;특정 대역&lt;/code&gt;에만 포트를 열어두는 것이 좋다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;관리망 (bastion)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;접근이 필요하므로 SSH(22) 포트를 개방한다.  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(Optional) bastion 서버가 살아있는지 보기 위해 ping을 사용하려면 ICMP - IPv4을 개방한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1437&quot; data-origin-height=&quot;89&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/npYcq/btq9JN41Ppg/GqFuNarFqCvjkB2ZQyWZ4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/npYcq/btq9JN41Ppg/GqFuNarFqCvjkB2ZQyWZ4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/npYcq/btq9JN41Ppg/GqFuNarFqCvjkB2ZQyWZ4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnpYcq%2Fbtq9JN41Ppg%2FGqFuNarFqCvjkB2ZQyWZ4k%2Fimg.png&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1437&quot; data-origin-height=&quot;89&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외부망 (application)&lt;ol&gt;
&lt;li&gt;외부에서 HTTP 포트로 접근하여 어플리케이션을 사용한다. HTTP 포트를 모두에게 개방한다. Anywhere-IPv4, IPv6을 선택하면 된다. HTTP 기본 포트가 아니라 다른 포트를 사용한 경우 TCP를 선택하고 해당 포트를 개방한다.&lt;/li&gt;
&lt;li&gt;HTTPS까지 사용하는 경우엔 HTTPS까지 개방한다.&lt;/li&gt;
&lt;li&gt;bastion에서 SSH로 접근해야 한다. 관리망 보안 그룹을 허용 대상으로 선택하면 해당 보안 그룹에 속한 모든 자원에 공통적으로 적용된다.&lt;/li&gt;
&lt;li&gt;(Optional) 관리망 보안 그룹에 대해 ICMP 포트를 개방하여 ping을 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;(Optional) 관리망 보안 그룹에 대해 telnet 포트를 개방하면 특정 포트가 살아있는지 확인할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;168&quot; data-filename=&quot;8.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l33bN/btq9EgOvavD/AO22YfhfCoIeXJedo75Ipk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l33bN/btq9EgOvavD/AO22YfhfCoIeXJedo75Ipk/img.png&quot; data-alt=&quot;&amp;amp;#39;모두&amp;amp;#39; 를 대상으로 개방&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l33bN/btq9EgOvavD/AO22YfhfCoIeXJedo75Ipk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl33bN%2Fbtq9EgOvavD%2FAO22YfhfCoIeXJedo75Ipk%2Fimg.png&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;168&quot; data-filename=&quot;8.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;'모두' 를 대상으로 개방&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;277&quot; data-filename=&quot;9.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EKhZJ/btq9LpW0XtB/LoaJlPRSSK88jDZXIuV6Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EKhZJ/btq9LpW0XtB/LoaJlPRSSK88jDZXIuV6Mk/img.png&quot; data-alt=&quot;보안 그룹을 대상으로 선택&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EKhZJ/btq9LpW0XtB/LoaJlPRSSK88jDZXIuV6Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEKhZJ%2Fbtq9LpW0XtB%2FLoaJlPRSSK88jDZXIuV6Mk%2Fimg.png&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;277&quot; data-filename=&quot;9.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;보안 그룹을 대상으로 선택&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;내부망 (인프라)&lt;ol&gt;
&lt;li&gt;mysql을 사용할 예정이기 때문에 mysql 기본 포트 3306을 개방한다. 대상은 외부망 보안 그룹으로 지정한다.&lt;/li&gt;
&lt;li&gt;기타 다른 인프라를 사용하는 경우 해당 포트를 외부망 보안 그룹에게 개방한다.&lt;/li&gt;
&lt;li&gt;bastion에서 SSH로 접근해야 한다. 이전에 설정한 관리망 보안 그룹을 선택하면 보안 그룹에 속한 모든 자원으로부터 SSH 접근이 가능하다.&lt;/li&gt;
&lt;li&gt;(Optional) 관리망 보안 그룹에 대해 ICMP 포트를 개방하면 ping을 사용하여 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;(Optional) 관리망 보안 그룹에 대해 telnet 포트를 개방하면 특정 포트가 살아있는지 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;(Optional) 보안 그룹 이야기는 아니지만, 구축 초기에 mysql 설치 등 이유로 인터넷에 접근이 필요할 수 있다. 그런 경우 서브넷 메뉴에 가서 내부망 그룹의 라우팅 테이블에 인터넷 게이트웨이를 임시로 추가하여 사용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;EC2 생성&lt;/h2&gt;
&lt;p&gt;여기서는 구성한 망을 적용하는 방법에 대해서만 다룬다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;EC2 생성 메뉴로 접속한다.&lt;/li&gt;
&lt;li&gt;OS는 입맛대로 선택한다. 가급적이면 LTS인 버전이 안정적이다.&lt;/li&gt;
&lt;li&gt;네트워크는 생성한 VPC를, 서브넷은 EC2의 용도에 따라 알맞게 선택한다.&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;94&quot; data-filename=&quot;_2021-07-15_22.58.15.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAgJQ4/btq9JLTNhrx/tkZ2QWLmenakrILppypPG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAgJQ4/btq9JLTNhrx/tkZ2QWLmenakrILppypPG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAgJQ4/btq9JLTNhrx/tkZ2QWLmenakrILppypPG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAgJQ4%2Fbtq9JLTNhrx%2FtkZ2QWLmenakrILppypPG0%2Fimg.png&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;94&quot; data-filename=&quot;_2021-07-15_22.58.15.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;나머지 옵션은 필요에 따라 적절하게 선택하고 생성하면 된다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Infra &amp;amp; Dev tools/Cloud platform</category>
      <category>AWS</category>
      <category>EC2</category>
      <category>VPC</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/125</guid>
      <comments>https://private-space.tistory.com/125#entry125comment</comments>
      <pubDate>Thu, 15 Jul 2021 23:13:40 +0900</pubDate>
    </item>
    <item>
      <title>AWS 망(VPC) 구성하기 (1) - CIDR 표기법</title>
      <link>https://private-space.tistory.com/124</link>
      <description>&lt;h1&gt;CIDR 표기법&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;사이더(Classless Inter-Domain Routing, CIDR)는 클래스 없는 도메인 간 라우팅 기법으로 1993년 도입되기 시작한, 최신의 IP 주소 할당 방법이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;네트워크를 잘 모르는 사람도 IP 주소가 무엇인지, 그리고 어떻게 생겼는지는 얼추 알고 있는 경우가 많다.&lt;/p&gt;
&lt;p&gt;CIDR 표기법은 IP 주소에 특별한 의미를 가진 숫자를 더하여 표기한다. (&lt;code&gt;192.168.50.0/24&lt;/code&gt; 같은 방식)&lt;/p&gt;
&lt;p&gt;CIDR을 제대로 이해하기 위해서는 IP 주소와 서브넷마스크에 대한 이해가 선행되어야 한다.&lt;/p&gt;
&lt;h2&gt;IP 주소&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;네트워크 자산의 논리적인 주소&lt;/li&gt;
&lt;li&gt;표기법&lt;ul&gt;
&lt;li&gt;0부터 255까지의 숫자를 4개 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;192.168.0.1&lt;/code&gt; 와 같은 방식으로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;영역&lt;ul&gt;
&lt;li&gt;IP 주소는 Network 영역과 Host 영역을 합쳐서 쓰는 형태이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;192.168.0.1&lt;/code&gt; 의 예시에서&lt;ul&gt;
&lt;li&gt;192.168이 네트워크 영역이라고 가정&lt;/li&gt;
&lt;li&gt;네트워크 영역이 아닌 부분은 모두 호스트 영역이다. 여기선 뒤의 두 개 숫자가 호스트 영역이 된다.&lt;/li&gt;
&lt;li&gt;192.168.x.x 의 형태로 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;x는 0 ~ 255 범위이므로 192.168 네트워크 대역의 호스트 개수는 256 * 256 = 65536개이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;네트워크 클래스&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC_%ED%81%B4%EB%9E%98%EC%8A%A4&quot;&gt;Reference&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;IP 주소를 효율적으로 사용하기 위해 정한 국제적인 규칙&lt;/li&gt;
&lt;li&gt;A, B, C, D, E 클래스가 존재&lt;/li&gt;
&lt;li&gt;IP 주소 중 첫 숫자를 2진법으로 변환한 뒤 시작 비트를 확인하면 클래스를 구분할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;A 클래스&lt;ul&gt;
&lt;li&gt;비트가 0으로 시작하는 숫자 (0000 0000 ~ 0111 1111 ⇒ 0 ~ 127)&lt;/li&gt;
&lt;li&gt;첫 번째 주소만 네트워크 주소이고 나머지 3개는 호스트 주소이다.&lt;/li&gt;
&lt;li&gt;124.0.0.1 는 A 클래스에 속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B 클래스&lt;ul&gt;
&lt;li&gt;비트가 10으로 시작하는 숫자 (1000 0000 ~ 1011 1111 ⇒ 128 ~ 191)&lt;/li&gt;
&lt;li&gt;앞의 두 숫자만 네트워크 주소이고 남은 2개는 호스트 주소이다.&lt;/li&gt;
&lt;li&gt;168.35.0.0 은 B 클래스에 속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;C 클래스&lt;ul&gt;
&lt;li&gt;비트가 110으로 시작하는 숫자 (1100 0000 ~ 1101 1111 ⇒ 192 ~ 223)&lt;/li&gt;
&lt;li&gt;앞의 3개 숫자가 네트워크 주소이고 마지막 1개는 호스트 주소이다.&lt;/li&gt;
&lt;li&gt;200.200.0.0 은 C 클래스에 속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;D 클래스&lt;ul&gt;
&lt;li&gt;비트가 1110으로 시작하는 숫자 (1110 0000 ~ 1110 1111 ⇒ 224 ~ 239)&lt;/li&gt;
&lt;li&gt;멀티캐스트를 위해 사용하는 주소 대역이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;E 클래스&lt;ul&gt;
&lt;li&gt;비트가 1111로 시작하는 숫자 (1111 0000 ~ 1111 1111 ⇒ 240 ~ 255)&lt;/li&gt;
&lt;li&gt;예약된 주소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A → E로 갈수록 시작 비트에 포함되는 1의 개수가 증가하는 것을 볼 수 있다.&lt;/p&gt;
&lt;h2&gt;서브넷 마스크&lt;/h2&gt;
&lt;p&gt;클래스만 사용하면 할당받은 IP 대역이 낭비되는 문제가 있다.&lt;/p&gt;
&lt;p&gt;이러한 문제를 해결하기 위해 서브넷 마스크 주소를 사용하여 IP를 조금 더 세밀하게 나누는 기법을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;어느 회사가 B 클래스 대역 1개를 할당받은 상황
  - B 클래스의 범위는  128.0.0.0 ~ 191.255.255.255
  - 여기서 네트워크 주소는 앞 2개 숫자를 사용한다.
  - 할당받은 대역이 130.10.x.x라고 가정
  - 앞의 두 개 숫자에 해당하는 130.10은 네트워크 영역이라 고정
  - 호스트 영역에 해당하는 x.x는 256개 * 256개 = 65536개이므로 최대 65536개의 IP를 사용할 수 있다.
  - 그러나 실제로 사용하는 IP가 10000개 뿐이라면 55536개가 낭비된다.&lt;/code&gt;&lt;/pre&gt;&lt;ol&gt;
&lt;li&gt;서브넷 마스크는 연속된 1이 먼저 나타나고 그 이후 0만 나타나도록 구성한다.&lt;ol&gt;
&lt;li&gt;255.255.255.0 ⇒ 11111111 11111111 11111111 00000000&lt;/li&gt;
&lt;li&gt;255.255.255.240 ⇒ 11111111 11111111 11111111 11110000&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;1의 개수로 네트워크 주소와 호스트 주소를 다시 나눈다. 네트워크 주소는 1이 닿는 범위까지이다.&lt;ol&gt;
&lt;li&gt;255.255.255.0은 24개의 1을 사용&lt;ol&gt;
&lt;li&gt;&lt;code&gt;130.10.20.30&lt;/code&gt; 이라는 IP가 있다면 130.10.20이 네트워크 주소&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;255.255.255.240은 28개의 1을 사용&lt;ol&gt;
&lt;li&gt;네트워크 주소가 앞 28개 비트, 호스트 주소가 뒤 4개 비트밖에 없다.&lt;/li&gt;
&lt;li&gt;이 대역은 호스트 주소가 16개이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;CIDR&lt;/h2&gt;
&lt;p&gt;CIDR을 이해하기 위한 준비가 끝났다.&lt;/p&gt;
&lt;p&gt;CIDR은 IP주소 + 서브넷마스크를 같이 표기한 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;192.168.50.0/24&lt;/code&gt; 의 예시&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/24&lt;/code&gt; 는 앞의 24개 비트를 네트워크 영역으로 사용하겠다는 의미이다.&lt;/li&gt;
&lt;li&gt;따라서 192.168.50이 네트워크 주소이고 &lt;code&gt;192.168.50.0 ~ 255&lt;/code&gt; 범위 내에서 호스트를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;192.168.30.32/28&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;서브넷마스크가 8의 배수로 나누어 떨어지지 않아 계산이 조금 난감할 수 있다.&lt;/li&gt;
&lt;li&gt;IP 주소의 전체 비트 수는 32개이고 28개 비트가 네트워크 영역이므로 호스트는 2^4 = 16개이다.&lt;/li&gt;
&lt;li&gt;시작 호스트는 32부터이다. 즉, 사용할 수 있는 IP는 &lt;code&gt;192.168.30.32&lt;/code&gt; ~ &lt;code&gt;192.168.30.47&lt;/code&gt; 이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;잘못된 정보에 대한 지적 및 내용 보충은 언제든지 환영합니다.&lt;/p&gt;</description>
      <category>Infra &amp;amp; Dev tools/Cloud platform</category>
      <category>AWS</category>
      <category>CIDR</category>
      <category>IP</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/124</guid>
      <comments>https://private-space.tistory.com/124#entry124comment</comments>
      <pubDate>Thu, 15 Jul 2021 01:07:12 +0900</pubDate>
    </item>
    <item>
      <title>2021년 상반기 및 우아한 테크캠프 Pro 2기 회고</title>
      <link>https://private-space.tistory.com/123</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;업계의 상반기 동향&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자라면 상반기 업계 동향에 대해 고민하지 않을 수 없는 시기였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유라면 당연히...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;333&quot; data-filename=&quot;스크린샷 2021-07-13 오후 6.06.08.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IOvd2/btq9tJwbfsQ/h5U9v8cTv7n5eHKHyCmOG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IOvd2/btq9tJwbfsQ/h5U9v8cTv7n5eHKHyCmOG0/img.png&quot; data-alt=&quot;개발자 연봉 대란&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IOvd2/btq9tJwbfsQ/h5U9v8cTv7n5eHKHyCmOG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIOvd2%2Fbtq9tJwbfsQ%2Fh5U9v8cTv7n5eHKHyCmOG0%2Fimg.png&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;333&quot; data-filename=&quot;스크린샷 2021-07-13 오후 6.06.08.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개발자 연봉 대란&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동요하지 않은 사람이 있을까 싶을 정도의 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;핫이슈&lt;/b&gt;&lt;/span&gt;가 업계를 강타하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 회사도 뒤늦게 동참하였고 다른 회사에 비해 크진 않지만 그래도 기분은 좋을 정도였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 내 상반기 회고에 왜 동향이 나오는가 하면...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;200&quot; data-filename=&quot;스크린샷 2021-07-13 오후 6.12.09.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pXOR1/btq9zFTlcPa/zaRBNR2M1OUi1gyIRDaBhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pXOR1/btq9zFTlcPa/zaRBNR2M1OUi1gyIRDaBhK/img.png&quot; data-alt=&quot;Github commit 내역&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pXOR1/btq9zFTlcPa/zaRBNR2M1OUi1gyIRDaBhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpXOR1%2Fbtq9zFTlcPa%2FzaRBNR2M1OUi1gyIRDaBhK%2Fimg.png&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;200&quot; data-filename=&quot;스크린샷 2021-07-13 오후 6.12.09.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Github commit 내역&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜 이직 준비에 지쳐버린 심신이 이 때를 기점으로 다시 살아났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 당장 나가지 않더라도 준비하지 않으면 도태될 것만 같은 위기감이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면서 1월 중순부터 매일 공부를 하는 습관을 들이게 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 연속 커밋을 시작한 날은 2021년 1월 15일이었고 몇 번의 실수를 제외하면 매일 꾸준히 커밋을 진행하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중 전사 연봉 일괄 인상안이 발표되면서 한시름 놓았는데 그 유혹이 너무 강해서 살짝 힘이 빠질뻔 했다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 꾸역꾸역 유지하려고 노력한 결과 관성이 생겨버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;데일리 커밋에서 느낀 점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전엔 데일리 커밋과 공부는 상관없으며 그저 쌓여가는 데이터를 위해 커밋을 한다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음... 사실 아직도 맞다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 그 의미가 조금 바뀌었는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부정적인 의미로 생각하면 오늘 뭔가 한 척 하기 위해서 대충 커밋을 반복하는 셈이 될테지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긍정적인 의미로 받아들인다면 공부를 위한 명분이 추가된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 사람이 공부를 계속 잡고 하려면 최소한의 명분이나 목표가 있어야 한다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미없는 공부는 본능적으로 안하는게 사람 아닐까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데일리 커밋은 이런 점에서 굉장히 긍정적인 습관이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루하루 데이터를 쌓아나가는게 생각보다 뿌듯한 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 처음 데일리 커밋을 시작했던 &quot;언제든 이직할 수 있는 준비&quot;는 이젠 크게 중요하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 이 좋은 습관을 이어나가는게 목표가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;아쉬운 점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부하는 습관을 들이고 커밋 로그를 하나씩 쌓아나가는건 좋지만 블로그도 함께 잘 하고 싶은 마음이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jojoldu님 같은 분과 비교해보면 이건 분명 나의 미숙함 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 커밋을 하자니 블로그가 힘들고, 블로그를 자주 작성하자니 커밋이 쉽지 않은 미묘한 상충관계가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 내용을 정리한 포스팅은 어짜피 검색하면 다 나오기 때문에 비중을 두고 싶지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나만의 정리, 나만의 컨텐츠를 만들고 싶었지만 그게 쉬운 일은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 어찌됐던 계속 잘해보고 싶은 마음은 있으니 양질의 컨텐츠를 더 고민해보아야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;우아한 테크캠프 2기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 7월엔 NextStep TDD 9기를 수강했다. 모든 미션을 완료하여 수료자 명단에 들 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막엔 번아웃이 찾아오면서 조금 늦긴 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어찌됐던 모든 미션을 다 해내며 굉장히 좋은 교육이라는 인상을 많이 받았던 기억이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 미션은 몇 단계의 step으로 나뉘고 하나를 완성하면 다음 단계로 나아가는 방식이었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완료 도장을 하나씩 찍어갈 때마다 성취감이 굉장히 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교육 기획을 어떻게 했는지는 모르겠지만 gamification 이론을 살짝 섞은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여튼! 우아한 테크캠프의 커리큘럼이 굉장히 좋다는 소문이 있어서 기대하고 있었는데 5월에 마침 딱 교육과정이 열렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교육을 받기 위해 긴 장문의 글을 쓰고... 프리코스 &lt;a href=&quot;https://github.com/dhmin5693/java-baseball-precourse/tree/dhmin5693&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;1주차&lt;/a&gt;... &lt;a href=&quot;https://github.com/dhmin5693/java-racingcar-precourse/tree/dhmin5693&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2주차&lt;/a&gt;... 미션을 완료했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년에 TDD를 선수강했던 경험이 많은 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;122&quot; data-filename=&quot;스크린샷 2021-07-13 23.07.53.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XYeUv/btq9s3BR8P5/kN6rkYj6MPw1qmNtZdGAH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XYeUv/btq9s3BR8P5/kN6rkYj6MPw1qmNtZdGAH0/img.png&quot; data-alt=&quot;합격!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XYeUv/btq9s3BR8P5/kN6rkYj6MPw1qmNtZdGAH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXYeUv%2Fbtq9s3BR8P5%2FkN6rkYj6MPw1qmNtZdGAH0%2Fimg.png&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;122&quot; data-filename=&quot;스크린샷 2021-07-13 23.07.53.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;합격!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미션은 총 8개가 있고 기간은 9주 (2021년 5월 17일 ~ 2021년 7월 19일 마감) 동안 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1등을 목표로 매일 새벽 늦게까지 공부했지만&amp;nbsp;8주차에 살짝 삐끗하면서 아쉽게도 1등은 놓치고 말았다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교육의 주제는 굉장히 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1주차는 TDD를 찍먹하며 객체지향에 대해 고민하는 시간을 갖는다. 찍먹이라 표현한 이유는 TDD는 별도의 강의로도 있지만 여기선 한 번의 미션만 진행하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차는 JPA이다. 개인적으로도 조금 공부해봤고 실무에도 살짝 쓰고 있지만(부서 특성 상 insert에 사용), 아직 사용법을 잘 모르는구나를 많이 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ATDD (+ TDD), AWS 인프라 구성 및 성능 최적화, 레거시 코드 리팩토링 등을 이어서 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열심히 공부한 결과 마지막 주차에 모든 미션을 마칠 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1345&quot; data-origin-height=&quot;158&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lfBL3/btq9zE1qVRd/ckkJ3XciIowafVYXauakF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lfBL3/btq9zE1qVRd/ckkJ3XciIowafVYXauakF1/img.png&quot; data-alt=&quot;모두 완료!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfBL3/btq9zE1qVRd/ckkJ3XciIowafVYXauakF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlfBL3%2Fbtq9zE1qVRd%2FckkJ3XciIowafVYXauakF1%2Fimg.png&quot; data-origin-width=&quot;1345&quot; data-origin-height=&quot;158&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모두 완료!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열심히 한 결과 우수 수료자 명단에도 들 수 있었다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의 하루, 못해도 2일에 1번 이상의 PR을 날렸고 실제로&amp;nbsp;이 기간동안 github 잔디의 대부분은 PR이나 리뷰에 남긴 답글이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 2달동안 이 이외의 공부를 할 시간이 거의 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔 잔디를 채우지 못하겠다 싶은 날은 아무 의미없는 야매 커밋을 한 날도 있다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 비양심에 한 가지 변명을 해보자면 아무 공부를 하지 않은 날은 야매 커밋도 하지 않으려 했다. 매일 공부해서 그런 날이 없을 뿐이다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열심히 공부했던 내용들이니 빨리 잊고 싶지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제를 진행하면서 공부했던 내용이나 느낀 점을 포스팅하여 남길 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;끝맺음&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나름 놀건 다 놀았기 때문에 마냥 놀면서 보낸줄로만 알았는데 기록으로만 보면 열심히 보낸 상반기처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 이직을 시작으로 방향점을 잡았다면, 이번 상반기는 성장하기 위한 습관을 잘 다진 시기라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 습관을 바탕으로 더 보람찬 하반기를 보내고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타/회고</category>
      <category>2021상반기</category>
      <category>우아한테크캠프2기</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/123</guid>
      <comments>https://private-space.tistory.com/123#entry123comment</comments>
      <pubDate>Tue, 13 Jul 2021 23:44:30 +0900</pubDate>
    </item>
    <item>
      <title>mac IntelliJ에서 창 전환 단축키가 안될 때</title>
      <link>https://private-space.tistory.com/122</link>
      <description>&lt;p&gt;Mac에서는 Command + ` 키를 활용하여 활성화된 프로그램 창을 전환할 수 있다.&lt;/p&gt;
&lt;p&gt;그러나 IntelliJ 에서는 이게 먹히지 않는다.&lt;/p&gt;
&lt;p&gt;일부 키를 바꿔주면 간단하게 고칠 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;command + , 를 눌러 Preferences로 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-04-21 01.47.50.png&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZf3K9/btq232IIm1B/KaDXrCPXQlIOWHr945ZgMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZf3K9/btq232IIm1B/KaDXrCPXQlIOWHr945ZgMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZf3K9/btq232IIm1B/KaDXrCPXQlIOWHr945ZgMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZf3K9%2Fbtq232IIm1B%2FKaDXrCPXQlIOWHr945ZgMk%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-04-21 01.47.50.png&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1680&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Next Project Window에 마우스를 대고 우클릭을 하면 할당된 키 조합인 Command + `를 삭제할 수 있다.&lt;/p&gt;
&lt;p&gt;그리고 Activate Next Window에 Command + `를 할당한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스크린샷 2021-04-21 01.49.35.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;1592&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebZZK7/btq27n59usi/WLE3k5fOM3IjoaPpFXAK81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebZZK7/btq27n59usi/WLE3k5fOM3IjoaPpFXAK81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebZZK7/btq27n59usi/WLE3k5fOM3IjoaPpFXAK81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FebZZK7%2Fbtq27n59usi%2FWLE3k5fOM3IjoaPpFXAK81%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-04-21 01.49.35.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;1592&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;두 기능의 키 조합을 바꿔주고 나면 창 전환이 잘 되는 것을 확인할 수 있다.&lt;/p&gt;</description>
      <category>Infra &amp;amp; Dev tools/Mac, Linux</category>
      <category>intellij</category>
      <category>Mac</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/122</guid>
      <comments>https://private-space.tistory.com/122#entry122comment</comments>
      <pubDate>Wed, 21 Apr 2021 01:51:36 +0900</pubDate>
    </item>
    <item>
      <title>Mac/리눅스에서 특정 포트 사용 여부를 쉽게 확인하는 방법</title>
      <link>https://private-space.tistory.com/121</link>
      <description>&lt;p&gt;개발을 하다 보면 특정 포트가 겹쳐서 어플리케이션이 에러를 뱉는 경우가 종종 발생하고는 한다.&lt;/p&gt;
&lt;p&gt;그럴때 포트를 사용중인지 확실하게 확인하는게 좋다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;lsof 명령어는 거의 대부분의 운영체제에서 사용할 수 있다고 하니 이 명령어를 활용해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1614962943238&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo lsof -nP -i4TCP:{PORT} | grep LISTEN&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;{PORT} 대신 8080처럼 확인하고자 하는 포트 번호를 입력하면 된다.&lt;/p&gt;
&lt;p&gt;하지만 이 명령어를 매번 치기도 귀찮다.&lt;/p&gt;
&lt;p&gt;그럴땐 쉘 스크립트의 function을 활용해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1614962749312&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;port() {
    sudo lsof -nP -i4TCP:$1 | grep LISTEN
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 function을 &lt;b&gt;~/.bashrc&lt;/b&gt;나 &lt;b&gt;~/.zshrc&lt;/b&gt; 등 쉘 실행과 동시에 같이 실행되는 파일에 저장한다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;port 8080&lt;/b&gt;과 같이 입력하여 사용하면 된다.&lt;/p&gt;</description>
      <category>Infra &amp;amp; Dev tools/Mac, Linux</category>
      <category>Linux</category>
      <category>Mac</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/121</guid>
      <comments>https://private-space.tistory.com/121#entry121comment</comments>
      <pubDate>Sat, 6 Mar 2021 01:55:00 +0900</pubDate>
    </item>
    <item>
      <title>리눅스 명령어의 결과를 한 번에 더하여 출력하기</title>
      <link>https://private-space.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어플리케이션이 메시지를 얼마나 빠르게 카프카 가져가고 있는지를 측정하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka를 리눅스 쉘에서 확인하면 current-offset 필드를 확인할 수 있는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 토픽의 모든 파티션 offset을 더하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1분 후에 offset을 더한 결과와 비교하는 방식으로 대략적인 분당 트래픽 처리량을 계산하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션이 적었을 땐 offset을 일일히 더해줘도 큰 문제가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;받는 데이터가 많아지면서&amp;nbsp;파티션을 몇 배나 증설하니 슬슬 수동으로 더하기 귀찮아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset은 항상 같은 컬럼에 있다는 규칙성이 있으니 한 번의 명령어로 해결하고자 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아는 명령어를 하나씩 파이프로 이어보고 모르는건 구글링을 해본 결과 아래처럼 입력하여 원하는 결과를 도출할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;ls -l | tail -n +2 | tr -s ' ' | cut -d ' ' -f2 | awk '{sum+=$1} END {print sum}'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 분석해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ls -l
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ls -l의 두 번째 필드는 링크 파일의 개수이다. 이 개수를 더하려고 한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;ls -l은 예시일 뿐이다. 원하는 명령어를 여기에 입력한다. 카프카 토픽 확인 명령어를 이 위치에 입력했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;tail -n +2
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ls -l의 첫 번째 줄은 total이고 두 번째 줄부터 파일 목록이 출력된다.&lt;/li&gt;
&lt;li&gt;불필요한 줄은 무시하고 출력하기 위한 옵션이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;tr -s ' '
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 필드마다 탭으로 구분이 되어 있다. 탭 대신 스페이스바 한 칸으로 구분하도록 변경한다.&lt;/li&gt;
&lt;li&gt;다음 명령어인 cut에서 구분자를 인식하기 위해 변경했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;cut -d ' ' -f2
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cut은 필드를 뽑아내는 명령어이다.&lt;/li&gt;
&lt;li&gt;-d는 구분자를 정하는 옵션으로, 기본값은 탭이다. 하지만 -d 없이 원하는 결과가 나오지 않아 tr 명령어를 먼저 사용했다.&lt;/li&gt;
&lt;li&gt;-f2는 구분자를 기준으로 2번째 필드만을 뽑아낸다. 각 줄마다 파일의 링크 개수가 뽑혀나온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;awk&amp;nbsp;'{sum+=$1}&amp;nbsp;END&amp;nbsp;{print&amp;nbsp;sum}'&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;awk 명령어를 활용하여 덧셈 및 출력을 하게 만들었다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진은 mac에서 /bin 디렉토리에 있는 파일들의 용량을 합한 결과이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-filename=&quot;스크린샷 2021-03-06 01.36.59.png&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;41&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVb4ZM/btqZpgb85Jd/QJCSZKWiXEhMPkAF6nZbB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVb4ZM/btqZpgb85Jd/QJCSZKWiXEhMPkAF6nZbB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVb4ZM/btqZpgb85Jd/QJCSZKWiXEhMPkAF6nZbB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVb4ZM%2FbtqZpgb85Jd%2FQJCSZKWiXEhMPkAF6nZbB0%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-03-06 01.36.59.png&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;41&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 방법으로 활용할 수가 있는데, 조금 변형하여 function으로 지정한 뒤 Kafka lag 남은 개수를 본다던가 하는 방법이 있다.&lt;/p&gt;</description>
      <category>Infra &amp;amp; Dev tools/Mac, Linux</category>
      <category>Linux</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/120</guid>
      <comments>https://private-space.tistory.com/120#entry120comment</comments>
      <pubDate>Sat, 6 Mar 2021 01:43:08 +0900</pubDate>
    </item>
    <item>
      <title>[LeetCode 4] Median of Two Sorted Arrays 해설</title>
      <link>https://private-space.tistory.com/119</link>
      <description>&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://leetcode.com/problems/median-of-two-sorted-arrays/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;leetcode.com/problems/median-of-two-sorted-arrays/&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1614878665118&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Median of Two Sorted Arrays - LeetCode&quot; data-og-description=&quot;Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.&quot; data-og-host=&quot;leetcode.com&quot; data-og-source-url=&quot;https://leetcode.com/problems/median-of-two-sorted-arrays/&quot; data-og-url=&quot;https://leetcode.com/problems/median-of-two-sorted-arrays/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fovSJ/hyJtLwsWrX/VbrT4WNuWCTyd2m8P0gRkK/img.png?width=500&amp;amp;height=260&amp;amp;face=0_0_500_260&quot;&gt;&lt;a href=&quot;https://leetcode.com/problems/median-of-two-sorted-arrays/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leetcode.com/problems/median-of-two-sorted-arrays/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fovSJ/hyJtLwsWrX/VbrT4WNuWCTyd2m8P0gRkK/img.png?width=500&amp;amp;height=260&amp;amp;face=0_0_500_260');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Median of Two Sorted Arrays - LeetCode&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;leetcode.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;1. 분류&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote class=&quot;tx-quote-tistory&quot;&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;Queue&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;2. 풀이&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;두 int 배열의 &lt;s&gt;평균&lt;/s&gt;이 아니라 &lt;b&gt;중앙값&lt;/b&gt;을 구하는 문제이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;두 배열을 합친 크기가 최대 2,000 밖에 안 되기 때문에&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;그냥 한 배열에 넣고 정렬한 뒤 중앙값 구해도 풀 수 있다. (...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;나는 두 우선순위 큐를 이용한 방법으로 해결했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;대부분의 언어에 구현되어 있는 우선순위 큐는 힙 기반이며 최소힙 / 최대힙을 사용할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 최소힙과 최대힙 1개씩을 만들어둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래 조건에 맞춰서 힙에 숫자를 넣는다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;큰 수는 최소 힙에, 작은 수는 최대 힙에 넣을 것이다.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최소 힙의 top은 최대 힙의 top보다 무조건 크다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최소 힙의 모든 원소는 최대 힙의 모든 원소보다 큰 값이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최소 힙의 크기는 최대 힙의 크기보다 크거나 같다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 보면 이해가 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;[1, 2, 3, 4, 5]
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최소 힙: [3, 4, 5] &amp;nbsp; (최대 힙의 최고 큰 숫자인 2보다 모두 크다)
&lt;ul style=&quot;list-style-type: circle;&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;top: 3&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최대 힙: [1, 2]
&lt;ul style=&quot;list-style-type: circle;&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;top: 2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;[1, 2, 3, 4, 5, 6]
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최소 힙: [4, 5, 6] &amp;nbsp; &lt;span style=&quot;color: #333333;&quot;&gt;(최대 힙의 최고 큰 숫자인 3보다 모두 크다)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;top: 4&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최대 힙: [1, 2, 3]
&lt;ul style=&quot;list-style-type: circle;&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;top: 3&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 정의한 힙 제약조건이 있기 때문에 아래 가정은 무조건 참이 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최소 힙 크기 = 최대 힙 크기인 경우 각 힙의 top을 더해 반으로 나눈 값 = 중앙값&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;최소 힙 크기 &amp;gt; 최대 힙 크기인 경우 최소 힙의 top = 중앙값&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;b&gt;3. 소스코드&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;3-1) 두 배열 합치고 정렬하는 단순한 방법&lt;/p&gt;
&lt;pre id=&quot;code_1614880115323&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Arrays;
import java.util.stream.IntStream;

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {

        int[] concat = IntStream.concat(Arrays.stream(nums1), Arrays.stream(nums2))
                                .sorted()
                                .toArray();

        if (concat.length == 1) {
            return concat[0];
        }

        if (concat.length % 2 == 0) {
            return (double)(concat[concat.length / 2 - 1] + concat[concat.length / 2]) / 2;
        }

        return concat[concat.length / 2];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3-2) 힙을 이용하는 방법&lt;/p&gt;
&lt;pre id=&quot;code_1614880135673&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Comparator;
import java.util.PriorityQueue;

class Solution {

    PriorityQueue&amp;lt;Integer&amp;gt; min = new PriorityQueue&amp;lt;&amp;gt;();
    PriorityQueue&amp;lt;Integer&amp;gt; max = new PriorityQueue&amp;lt;&amp;gt;(Comparator.reverseOrder());

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {

        enqueue(nums1);
        enqueue(nums2);

        if (min.size() &amp;gt; max.size()) {
            return (double) min.peek();
        }

        return (((double) min.peek()) + ((double) max.peek())) / 2f;
    }

    private void enqueue(int[] nums) {
        for (int num : nums) {
            if (min.size() &amp;lt;= max.size()) {
                min.add(num);
            } else {
                max.add(num);
            }

            if (min.size() == 0 || max.size() == 0) {
                continue;
            }

            if (min.peek() &amp;lt;= max.peek()) {
                int a = min.poll();
                int b = max.poll();

                max.add(a);
                min.add(b);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;- 테스트 코드 -&lt;/p&gt;
&lt;pre id=&quot;code_1614880167938&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class SolutionTest {

    private final Solution code = new Solution();

    @MethodSource(&quot;testcase&quot;)
    @ParameterizedTest
    void test(int[] nums1, int[] nums2, double output) {

        var result = code.findMedianSortedArrays(nums1, nums2);
        System.out.println(&quot;result: &quot; + result);
        System.out.println(&quot;output: &quot; + output);

        assertThat(result, is(output));
    }

    private static Stream&amp;lt;Arguments&amp;gt; testcase() {
        return Stream.of(
            Arguments.of(new int[] {1,3}, new int[] {2}, 2.00000),
            Arguments.of(new int[] {0,0}, new int[] {0,0}, 0.00000),
            Arguments.of(new int[] {}, new int[] {1}, 1.00000),
            Arguments.of(new int[] {2}, new int[] {}, 2.00000)
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘</category>
      <category>알고리즘</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/119</guid>
      <comments>https://private-space.tistory.com/119#entry119comment</comments>
      <pubDate>Fri, 5 Mar 2021 02:49:56 +0900</pubDate>
    </item>
    <item>
      <title>2021 KAKAO BLIND RECRUITMENT 3번[검색 순위] 해설</title>
      <link>https://private-space.tistory.com/117</link>
      <description>&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;programmers.co.kr/learn/courses/30/lessons/72412&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1614011388389&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코딩테스트 연습 - 순위 검색&quot; data-og-description=&quot;[&amp;quot;java backend junior pizza 150&amp;quot;,&amp;quot;python frontend senior chicken 210&amp;quot;,&amp;quot;python frontend senior chicken 150&amp;quot;,&amp;quot;cpp backend senior pizza 260&amp;quot;,&amp;quot;java backend junior chicken 80&amp;quot;,&amp;quot;python backend senior chicken 50&amp;quot;] [&amp;quot;java and backend and junior and pizza 100&amp;quot;,&amp;quot;pyt&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot; data-og-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b4X4Ya/hyJmCgdrBf/PEWmSL5h5KIufLYUpS4h1k/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626,https://scrap.kakaocdn.net/dn/clwVq7/hyJmxeUylC/ZJPaXfgpOVZHyyJaTkiSd1/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626&quot;&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b4X4Ya/hyJmCgdrBf/PEWmSL5h5KIufLYUpS4h1k/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626,https://scrap.kakaocdn.net/dn/clwVq7/hyJmxeUylC/ZJPaXfgpOVZHyyJaTkiSd1/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;코딩테스트 연습 - 순위 검색&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;[&quot;java backend junior pizza 150&quot;,&quot;python frontend senior chicken 210&quot;,&quot;python frontend senior chicken 150&quot;,&quot;cpp backend senior pizza 260&quot;,&quot;java backend junior chicken 80&quot;,&quot;python backend senior chicken 50&quot;] [&quot;java and backend and junior and pizza 100&quot;,&quot;pyt&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;1. 분류&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote class=&quot;tx-quote-tistory&quot;&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;이분탐색&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;2. 풀이&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;문제 자체를 이해하기는 어렵지 않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;데이터베이스를 활용해 본 경험이 있다면 이 문제가 아래의 쿼리 결과를 요구한다는 것을 바로 눈치챌 수 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614011812566&quot; class=&quot;html xml&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background-color: #f6f7f8; font-size: 14px; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: initial initial; background-repeat: initial initial;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- INPUT에서 데이터 삽입
INSERT INTO TABLE(LANGUAGE, POSITION, LEVEL, FOOD, SCORE)
VALUES (${language}, ${position}, ${level}, ${food}, ${score});

-- 주어진 query로 개수 탐색
SELECT count(*)
FROM   TABLE
WHERE  [language = 'java', position = 'backend' 등 주어진 조건에 따라 가변처리]
AND    SCORE &amp;gt;= ${score}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 쉽다. DB를 쓸 수 있다면 5분도 걸리지 않을 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코드로만 구현해야 하는 제약조건에서 &quot;-&quot; 으로 주어지는 dont care 조건을 어떻게 구현해야 할까를 고민해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;List&amp;lt;Integer&amp;gt;[LANGUAGE][POSITION][LEVEL][FOOD]&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;에 넣는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인덱스를 순서대로 기입함에 주의한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dont care를 찾아내기 위한 가장 쉬운 답은 dont care 데이터까지 함께 삽입해버리는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 [java backend junior chicken 300]이라는 데이터를 받았다고 한다면,&lt;/p&gt;
&lt;pre id=&quot;code_1614012223225&quot; class=&quot;html xml&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background-color: #f6f7f8; font-size: 14px; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: initial initial; background-repeat: initial initial;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java, backend, junior, chicken   (원본)
java, backend, junior, *         (*은 dont care)
java, backend, *, chicken
java, backend, *, *
java, *, junior, chicken
java, *, junior, *
java, *, *, chicken
java, *, *, *
*, backend, junior, chicken
*, backend, junior, *
*, backend, *, chicken
*, backend, *, *
*, *, junior, chicken
*, *, junior, *
*, *, *, chicken
*, *, *, *&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 16개의 데이터를 함께 삽입해버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리가 java and - and - and - 150으로 주어지면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;list[JAVA_INDEX][DONTCARE_INDEX]&lt;span style=&quot;color: #333333;&quot;&gt;[DONTCARE_INDEX]&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;[DONTCARE_INDEX]&lt;/b&gt;를 찾아내면 되는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 데이터를 삽입하고 난 뒤엔 모든 List를 오름차순으로 정렬한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;350점 이상을 찾으려고 할 때, 같은 조건 안에 350점이 여러 명 있을수도 있고 350점이 없을수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬된 리스트에서 이진 탐색으로 적절한 값을 찾되, 그 중에서도 가장 왼쪽의 인덱스를 얻어내기 위해서는 lower bound를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;b&gt;3. 소스코드&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;풀이&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

class Solution {

    private final ArrayList&amp;lt;Integer&amp;gt;[][][][] database = new ArrayList[4][3][3][3];

    public int[] solution(String[] infos, String[] queries) {

        Arrays.stream(infos)
              .forEach(this::insert);

        sortDatabase();

        return Arrays.stream(queries)
                     .mapToInt(this::search)
                     .toArray();
    }

    private void insert(String info) {

        String[] column = split(info);

        int[] indexes = new int[4];

        for (int i = 0; i &amp;lt; 4; i++) {
            indexes[i] = Constant.findIndex(column[i]);
        }

        int score = Integer.parseInt(column[4]);
        int dontCare = Constant.DONT_CARE.index;

        for (int i = 0; i &amp;lt; 16; i++) {
            int idx0 = (i &amp;amp; 1) == 1 ? dontCare : indexes[0];
            int idx1 = (i &amp;amp; 2) == 2 ? dontCare : indexes[1];
            int idx2 = (i &amp;amp; 4) == 4 ? dontCare : indexes[2];
            int idx3 = (i &amp;amp; 8) == 8 ? dontCare : indexes[3];
            addScore(idx0, idx1, idx2, idx3, score);
        }
    }

    private void sortDatabase() {
        for (int i = 0; i &amp;lt; 4; i++) {
            for (int j = 0; j &amp;lt; 3; j++) {
                for (int k = 0; k &amp;lt; 3; k++) {
                    for (int l = 0; l &amp;lt; 3; l++) {
                        if (database[i][j][k][l] != null) {
                            Collections.sort(database[i][j][k][l]);
                        }
                    }
                }
            }
        }
    }

    private int search(String query) {

        String[] data = split(query);

        int language = Constant.findIndex(data[0]);
        int position = Constant.findIndex(data[2]);
        int level = Constant.findIndex(data[4]);
        int food = Constant.findIndex(data[6]);

        int score = Integer.parseInt(data[7]);

        return binarySearch(database[language][position][level][food], score);
    }

    private int binarySearch(List&amp;lt;Integer&amp;gt; list, int score) {

        if (list == null || list.size() == 0) {
            return 0;
        }

        if (list.size() == 1) {
            if (score &amp;lt;= list.get(0)) {
                return 1;
            }
            return 0;
        }

        int left = 0;
        int right = list.size();

        while (left &amp;lt; right) {
            int middle = (left + right) / 2;

            if (list.get(middle) &amp;lt; score) {
                left = middle + 1;
            } else {
                right = middle;
            }
        }

        return list.size() - left;
    }

    private void addScore(int idx0, int idx1, int idx2, int idx3, int score) {
        if (database[idx0][idx1][idx2][idx3] == null) {
            database[idx0][idx1][idx2][idx3] = new ArrayList&amp;lt;&amp;gt;();
        }
        database[idx0][idx1][idx2][idx3].add(score);
    }

    private String[] split(String s) {
        return s.split(&quot; &quot;);
    }

    enum Constant {
        ERROR(null, -1),
        DONT_CARE(&quot;-&quot;, 0),
        CPP(&quot;cpp&quot;, 1),
        JAVA(&quot;java&quot;, 2),
        PYTHON(&quot;python&quot;, 3),
        BACKEND(&quot;backend&quot;, 1),
        FRONTEND(&quot;frontend&quot;, 2),
        JUNIOR(&quot;junior&quot;, 1),
        SENIOR(&quot;senior&quot;, 2),
        CHICKEN(&quot;chicken&quot;, 1),
        PIZZA(&quot;pizza&quot;, 2);

        private static final Map&amp;lt;String, Constant&amp;gt; ALL_MAP =
            Arrays.stream(values())
                  .collect(Collectors.toMap(c -&amp;gt; c.value, Function.identity()));

        String value;
        int index;

        Constant(String value, int index) {
            this.value = value;
            this.index = index;
        }

        public static int findIndex(String key) {
            return ALL_MAP.getOrDefault(key, ERROR).index;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;b&gt;ALL_MAP&lt;/b&gt;은 문자열 비교 시간을 아끼기 위해 (Constant.value, Constant)를 map으로 미리 구성했다.&lt;/p&gt;
&lt;p&gt;map에서 찾아오기 때문에 이 문제에서 모든 문자열을 탐색하는 시간은 O(1)으로 고정된다. (문자열로 해시값을 만드는 시간 제외)&lt;/p&gt;
&lt;p&gt;실무에서도 상수성 데이터를 지속적으로 비교하는 경우 굉장히 유용한 테크닉이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트 (실행)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614011140367&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class SolutionTest {

    private final Solution s = new Solution();

    @MethodSource(&quot;testcase&quot;)
    @ParameterizedTest
    void test(String[] infos, String[] queries, int[] output) {

        var result = s.solution(infos, queries);
        System.out.println(&quot;result: &quot; + Arrays.toString(result));
        System.out.println(&quot;output: &quot; + Arrays.toString(output));

        assertThat(result, is(output));
    }

    private static Stream&amp;lt;Arguments&amp;gt; testcase() {
        return Stream.of(
            Arguments.of(
                new String[] {&quot;java backend junior pizza 150&quot;,&quot;python frontend senior chicken 210&quot;,&quot;python frontend senior chicken 150&quot;,&quot;cpp backend senior pizza 260&quot;,&quot;java backend junior chicken 80&quot;,&quot;python backend senior chicken 50&quot;},
                new String[] {&quot;java and backend and junior and pizza 100&quot;,&quot;python and frontend and senior and chicken 200&quot;,&quot;cpp and - and senior and pizza 250&quot;,&quot;- and backend and senior and - 150&quot;,&quot;- and - and - and chicken 100&quot;,&quot;- and - and - and - 150&quot;},
                new int[] {1,1,1,1,2,4})
        );
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘</category>
      <category>블라인드공채</category>
      <category>알고리즘</category>
      <category>카카오</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/117</guid>
      <comments>https://private-space.tistory.com/117#entry117comment</comments>
      <pubDate>Tue, 23 Feb 2021 02:01:34 +0900</pubDate>
    </item>
    <item>
      <title>2021 KAKAO BLIND RECRUITMENT 2번[메뉴 리뉴얼] 해설</title>
      <link>https://private-space.tistory.com/115</link>
      <description>&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;링크&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/72411&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;programmers.co.kr/learn/courses/30/lessons/72411&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1613574397454&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코딩테스트 연습 - 메뉴 리뉴얼&quot; data-og-description=&quot;레스토랑을 운영하던 스카피는 코로나19로 인한 불경기를 극복하고자 메뉴를 새로 구성하려고 고민하고 있습니다. 기존에는 단품으로만 제공하던 메뉴를 조합해서 코스요리 형태로 재구성해서&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72411&quot; data-og-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72411&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bp6zoj/hyJiFeeEL5/JUtGl8iYBLZUPNyL6KTc70/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626,https://scrap.kakaocdn.net/dn/eIfPq/hyJiEsReXB/5pLQh5fx74kjMuYawQwLJk/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626&quot;&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/72411&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72411&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bp6zoj/hyJiFeeEL5/JUtGl8iYBLZUPNyL6KTc70/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626,https://scrap.kakaocdn.net/dn/eIfPq/hyJiEsReXB/5pLQh5fx74kjMuYawQwLJk/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;코딩테스트 연습 - 메뉴 리뉴얼&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;레스토랑을 운영하던 스카피는 코로나19로 인한 불경기를 극복하고자 메뉴를 새로 구성하려고 고민하고 있습니다. 기존에는 단품으로만 제공하던 메뉴를 조합해서 코스요리 형태로 재구성해서&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;1. 분류&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote class=&quot;tx-quote-tistory&quot;&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;백트래킹&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: =;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;2. 풀이&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;음식점은 손님이 &lt;b&gt;최소 2번 이상 주문한 단품 메뉴 중 가장 많이 주문한 단품 메뉴&lt;/b&gt;를 골라서 코스 요리를 새로 만들고자 한다.&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;입력으로 1번 ~ n번 손님이 주문한 메뉴 목록, 코스 요리로 구성할 단품&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;메뉴의 개수가 주어지고,&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;출력은 새로 구성한 코스요리 목록을 내보내야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;제한사항을 먼저 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;최대 크기가 전부 10의 자리로, 꽤 작은 편이다.&lt;/p&gt;
&lt;p&gt;그렇다면 완전탐색으로 해결할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;작성한 로직은 아래 순서를 따른다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;손님이 주문한 단품 요리 목록(orders)에서 단품 메뉴의 조합을 구한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;손님이 주문한 메뉴의 개수가 n개이며 순서는 무조건 알파벳순이다.&lt;/li&gt;
&lt;li&gt;조합의 수는 nC0 + nC1 + nC2 + ... nCn = n^2 (단 문제 조건에 의해 2개 이상 조합만 구해야 하므로 nC0, nC1은 빼준다)&lt;/li&gt;
&lt;li&gt;따라서 조합을 구하는 시간복잡도는 &lt;b&gt;O(n^2)&lt;/b&gt;이 된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;손님이 m명이고 각 손님이 n개를 주문했다고 가정하면 &lt;b&gt;O(n^2 * m)&lt;/b&gt;가 된다.&lt;/li&gt;
&lt;li&gt;입력에서 주어진 &lt;b&gt;[코스 요리의 단품 메뉴 개수]&lt;/b&gt;에 맞는 코스 요리만 선정한다.&lt;/li&gt;
&lt;li&gt;모든 손님의 주문 메뉴를 모아서 조합을 구하려는 생각은 좋지 않다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;전혀 매칭되지 않는 코스요리 메뉴가 나올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;1에서 구한 단품 메뉴 조합의 중복을 제거한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단품 메뉴를 저장할 때 set같은 자료구조를 사용하면 간편하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;손님의 주문 목록을 순회하며 단품 메뉴 조합의 주문 회수를 구한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드에선 computeMenuCounter가 담당한다.&lt;/li&gt;
&lt;li&gt;set의 containsAll을 활용하기 위해 메뉴를 Set&amp;lt;Character&amp;gt;로 변환하여 전달했다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;3에서 구한 결과를 주문 회수로 내림차순 정렬한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;entry는 map에 저장된 key, value 쌍이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;4에서 구한 결과를 순회하며 입력에서 주어진 &lt;b&gt;[코스 요리의 단품 메뉴 개수]&lt;/b&gt;에 맞는 결과를 뽑아낸다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;b&gt;3. 소스코드&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Solution&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

class Solution {

    private int[] courses;
    private final StringBuilder current = new StringBuilder();
    private final Set&amp;lt;String&amp;gt; combinations = new HashSet&amp;lt;&amp;gt;();

    public String[] solution(String[] orders, int[] courses) {

        this.courses = courses;

        for (String order : orders) {
            var orderArray = order.toCharArray();
            Arrays.sort(orderArray);
            combination(orderArray, 0, order.length());
        }

        var menuCounter = computeMenuCounter(
            Arrays.stream(orders)
                  .map(this::toCharSet)
                  .collect(Collectors.toList()));

        var entries = getSortedEntries(menuCounter);

        return getAnswer(entries);
    }

    private void combination(char[] orderChars, int index, int max) {

        if (index &amp;gt;= max) {
            return;
        }

        for (int i = index; i &amp;lt; max; i++) {

            current.append(orderChars[i]);
            int length = current.length();
            for (int course : courses) {
                if (length == course) {
                    combinations.add(current.toString());
                    break;
                }
            }

            combination(orderChars, i + 1, max);
            current.deleteCharAt(length - 1);
        }
    }

    private Map&amp;lt;String, Integer&amp;gt; computeMenuCounter(List&amp;lt;Set&amp;lt;Character&amp;gt;&amp;gt; ordersSet) {

        var map = new HashMap&amp;lt;String, Integer&amp;gt;();

        for (var orderSet : ordersSet) {
            for (var comb : combinations) {
                if (contains(orderSet, comb)) {
                    map.put(comb, map.getOrDefault(comb, 0) + 1);
                }
            }
        }

        return map;
    }

    private ArrayList&amp;lt;Entry&amp;lt;String, Integer&amp;gt;&amp;gt; getSortedEntries(Map&amp;lt;String, Integer&amp;gt; map) {

        var list = new ArrayList&amp;lt;&amp;gt;(map.entrySet());
        list.sort((o1, o2) -&amp;gt; o2.getValue() - o1.getValue());

        return list;
    }

    private String[] getAnswer(ArrayList&amp;lt;Entry&amp;lt;String, Integer&amp;gt;&amp;gt; entries) {

        var result = new ArrayList&amp;lt;String&amp;gt;();
        var maxCounts = new HashMap&amp;lt;Integer, Integer&amp;gt;();

        for (var entry : entries) {

            int count = entry.getValue();
            if (count &amp;lt;= 1) {
                continue;
            }

            int courseSize = entry.getKey().length();

            if (count &amp;gt;= maxCounts.getOrDefault(courseSize, 0)) {
                maxCounts.put(courseSize, count);
                result.add(entry.getKey());
            }
        }

        Collections.sort(result);

        var answer = new String[result.size()];
        for (int i = 0; i &amp;lt; result.size(); i++) {
            answer[i] = result.get(i);
        }

        return answer;
    }

    private Stream&amp;lt;Character&amp;gt; stringToCharStream(String s) {
        return s.chars().mapToObj(i -&amp;gt; (char) i);
    }

    private boolean contains(Set&amp;lt;Character&amp;gt; set, String s) {
        return stringToCharStream(s).allMatch(set::contains);
    }

    private Set&amp;lt;Character&amp;gt; toCharSet(String s) {
        return stringToCharStream(s).collect(Collectors.toSet());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;TestCode&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1613576318803&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class SolutionTest {

    private final Solution s = new Solution();

    @MethodSource(&quot;testcase&quot;)
    @ParameterizedTest
    void test(String[] orders, int[] courses, String[] output) {

        var result = s.solution(orders, courses);
        System.out.println(Arrays.asList(result));
        System.out.println(Arrays.asList(output));

        for (int i = 0; i &amp;lt; output.length; i++) {
            assertEquals(result[i], output[i]);
        }
    }

    private static Stream&amp;lt;Arguments&amp;gt; testcase() {
        return Stream.of(
            Arguments.of(new String[] {&quot;ABCFG&quot;, &quot;AC&quot;, &quot;CDE&quot;, &quot;ACDE&quot;, &quot;BCFG&quot;, &quot;ACDEH&quot;}, new int[] {2,3,4}, new String[] {&quot;AC&quot;, &quot;ACDE&quot;, &quot;BCFG&quot;, &quot;CDE&quot;}),
            Arguments.of(new String[] {&quot;ABCDE&quot;, &quot;AB&quot;, &quot;CD&quot;, &quot;ADE&quot;, &quot;XYZ&quot;, &quot;XYZ&quot;, &quot;ACD&quot;},	new int[] {2,3,5}, new String[] {&quot;ACD&quot;, &quot;AD&quot;, &quot;ADE&quot;, &quot;CD&quot;, &quot;XYZ&quot;}),
            Arguments.of(new String[] {&quot;XYZ&quot;, &quot;XWY&quot;, &quot;WXA&quot;}, new int[] {2,3,4}, new String[] {&quot;WX&quot;, &quot;XY&quot;})
        );
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/115</guid>
      <comments>https://private-space.tistory.com/115#entry115comment</comments>
      <pubDate>Thu, 18 Feb 2021 00:44:40 +0900</pubDate>
    </item>
    <item>
      <title>url을 복사하고 붙여넣을 때 page title이 뜨는 현상 해결하기</title>
      <link>https://private-space.tistory.com/113</link>
      <description>&lt;p&gt;엣지 브라우저를 쓰고 있는데,&lt;/p&gt;
&lt;p&gt;언젠가부터 URL을 복사-붙여넣기하면 URL이 쓰이는게 아니라 page title로 자동 변형되어서 붙여지곤 했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들면 이렇다. 웹 브라우저의 URL을 복사한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;스크린샷 2021-02-05 오전 11.46.09.png&quot; data-origin-width=&quot;218&quot; data-origin-height=&quot;34&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnw8I4/btqVQWIChy0/IrEY640DWuJLkBthMSm0uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnw8I4/btqVQWIChy0/IrEY640DWuJLkBthMSm0uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnw8I4/btqVQWIChy0/IrEY640DWuJLkBthMSm0uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnw8I4%2FbtqVQWIChy0%2FIrEY640DWuJLkBthMSm0uK%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-05 오전 11.46.09.png&quot; data-origin-width=&quot;218&quot; data-origin-height=&quot;34&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 붙여넣는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;176&quot; data-filename=&quot;스크린샷 2021-02-05 오전 11.46.59.png&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wbvBM/btqVIJcqUBk/i6YeEG1HRgVzNLKgz3YQRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wbvBM/btqVIJcqUBk/i6YeEG1HRgVzNLKgz3YQRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wbvBM/btqVIJcqUBk/i6YeEG1HRgVzNLKgz3YQRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwbvBM%2FbtqVIJcqUBk%2Fi6YeEG1HRgVzNLKgz3YQRk%2Fimg.png&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;176&quot; data-filename=&quot;스크린샷 2021-02-05 오전 11.46.59.png&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;URL이 붙여넣어지길 기대했으나 (https://www.google.com) Google이라는 글자만 나타난다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;윈도우에선 그런 일이 없어서 맥 문제인가, 빅서 문제인가를 한참 찾아봤는데 결론은 edge의 설정 문제였다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 설정에 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;스크린샷 2021-02-05 오전 11.20.45.png&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;652&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKaKqi/btqVOrPEY6g/B2oOggkBnxvR8ePRUjDPR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKaKqi/btqVOrPEY6g/B2oOggkBnxvR8ePRUjDPR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKaKqi/btqVOrPEY6g/B2oOggkBnxvR8ePRUjDPR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKaKqi%2FbtqVOrPEY6g%2FB2oOggkBnxvR8ePRUjDPR0%2Fimg.png&quot; data-filename=&quot;스크린샷 2021-02-05 오전 11.20.45.png&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;652&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 아래 메뉴에서 일반 텍스트를 선택한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkg4Py/btqVSvcJHQh/Krir65j3VpHWaiRMm0nVD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkg4Py/btqVSvcJHQh/Krir65j3VpHWaiRMm0nVD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkg4Py/btqVSvcJHQh/Krir65j3VpHWaiRMm0nVD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkg4Py%2FbtqVSvcJHQh%2FKrir65j3VpHWaiRMm0nVD1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;원하는대로 URL이 정상적으로 복사가 됨을 확인할 수 있다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>Chrome</category>
      <category>EDGE</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/113</guid>
      <comments>https://private-space.tistory.com/113#entry113comment</comments>
      <pubDate>Fri, 5 Feb 2021 11:48:19 +0900</pubDate>
    </item>
    <item>
      <title>git 공부하기 좋은 웹사이트</title>
      <link>https://private-space.tistory.com/112</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://learngitbranching.js.org/?locale=ko&quot;&gt;https://learngitbranching.js.org/?locale=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1612492847064&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Learn Git Branching&quot; data-og-description=&quot;An interactive Git visualization tool to educate and challenge!&quot; data-og-host=&quot;learngitbranching.js.org&quot; data-og-source-url=&quot;https://learngitbranching.js.org/?locale=ko&quot; data-og-url=&quot;https://pcottle.github.io/learnGitBranching/index.html?demo&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cys9d7/hyJabqxqSJ/QBQnypjux8RAoFnKSlymnK/img.png?width=959&amp;amp;height=647&amp;amp;face=0_0_959_647&quot;&gt;&lt;a href=&quot;https://learngitbranching.js.org/?locale=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learngitbranching.js.org/?locale=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cys9d7/hyJabqxqSJ/QBQnypjux8RAoFnKSlymnK/img.png?width=959&amp;amp;height=647&amp;amp;face=0_0_959_647');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Learn Git Branching&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;An interactive Git visualization tool to educate and challenge!&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;learngitbranching.js.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Infra &amp;amp; Dev tools/Git</category>
      <category>git</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/112</guid>
      <comments>https://private-space.tistory.com/112#entry112comment</comments>
      <pubDate>Fri, 5 Feb 2021 11:41:51 +0900</pubDate>
    </item>
    <item>
      <title>주니어 개발자의 이직 이야기</title>
      <link>https://private-space.tistory.com/111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;작년 중순, &lt;a href=&quot;https://programmers.co.kr/competitions/144/2020-web-be-first&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;프로그래머스 데브 잡 매칭&lt;/b&gt;&lt;/a&gt;을 통해 이직했다. (궁금하시다면 링크를 눌러 확인해보시길)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 안되면 나란 사람에 대해 정말 심각하게 고민해보자는 마음으로 임했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;첫 직장&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2017년 하반기, 내 나름대로 기준을 세워가며 가고 싶은 회사를 고르고 골랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 직장도 그 기준에 부합했고 공채를 통과하여 2018년 신입사원으로 시작할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말하면 더 원했던 회사는 떨어지긴 했다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경험은 누구나 있을테니 제쳐두고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 돈벌이를 한다는 생각에 그저 좋았던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 2018년 하반기부터 마냥 철없이 이직을 시도했다. 급여가 너무 적었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 마음가짐으로도 대기업에 다시 안착하는 중고신입도 굉장히 많지만 내겐 그런 운과 실력은 없었던 모양이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 계속 신입공채 준비만 하며 2년을 의미없이 보냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;당신은 왜 이직하나요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직에서 가장 중요한 것이 &lt;u&gt;&lt;b&gt;&quot;왜 이직하느냐&quot;&lt;/b&gt;&lt;/u&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;터전을 다른 곳으로 옮기려는 데에는 마땅한 이유가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;지금 잘 다니는데 굳이 옮겨야만 할까? 왜? 면접관도 제일 궁금해하는 질문이기도 하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;내가 면접관이어도 조금 더 진솔한 이유를 가진 사람을 뽑을 것이다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실력은 기술 면접을 통해 검증하면 된다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 나처럼 현실적인 이유일 수도 있다. 급여가 부족해서, 복지가 안좋아서, 업무 환경이 좋지 않아서.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;나는 어떤 마음으로 이직을 준비했을까&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신입 공채에 붙어 새 회사에 옮기더라도 지금 일(스프링 백엔드)를 계속 하고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 일이 재밌고 마음에 들었기 때문이다. 그리고 다른 환경에서 더 잘해보고 싶은 마음이 강했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3년차가 되었을 때 중고신입으로 가기는 늦었다고 생각했다. 중고 신입은 보통 2년차까지 받기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면서 내 마음가짐도 조금 수정할 필요를 느꼈던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실하게 지금 일을 더 잘할 수 있는 곳을 골라보자고 마음먹었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직해야 하는 가장 중요한 이유를 &lt;b&gt;성장&lt;/b&gt;으로 바꾸었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;환경과 성장&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직 사유와는 상관 없이 2년동안 회사를 마음 편히 다니지 못하고 항상 불안했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커리어에 대한 고민.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이대로 시간을 계속 보낸다면 그저 이런 일을 반복하는 개발자가 되겠구나 싶은 무서움.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내면의 불안감을 떨쳐낼만한 회사를 골라야만 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사내 환경은 주니어 서버 개발자가 성장하기에 좋지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 큰 이유는 분위기다. 같이 공부하고 어떻게 개선할까 하는 고민을 같이 했었으면 좋겠는데 그렇지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선은 언제나 좋다고들 말하지만 개선을 적용하려면 확실한 &quot;&lt;b&gt;Benefit&lt;/b&gt;&quot;이 있어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;네가 말한 &lt;b&gt;benefit&lt;/b&gt;은 알겠어. 근데 내 경험상...&quot; 으로 시작하는 경험 중심의 시니어를 설득하기는 너무 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 경험은 틀리지 않다. 그러나 영원히 정답은 아니다. IT는 온고지신의 마음가짐으로는 안되는 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 언급했던 개선점들은 업계에서 이미 널리 쓰이고 있는 요소들이었고 분명 같이 연구할만한 가치는 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일례로 팀 차원에서 Git으로 이관하기 위해 내가 총대를 매고 Gitlab 서버를 구축했던 적이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내에서 사용하지 않던 컨테이너 환경으로 구축하기도 하고 팀을 위해 git flow 전략을 고민해보는 등 나름대로 연구를 많이 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나&amp;nbsp;Git의 장점인 멀티 브랜치는 SVN에서도 잘 되는데 프로세스를 바꾸어가면서 왜 전환해야 하느냐고 하는 시니어 분을 설득하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 설득하지 못한 내가 가장 큰 잘못이다. &lt;span style=&quot;color: #333333;&quot;&gt;팀장님도 내 편이었고 주니어도 내 편이었는데도 내 목소리는 닿지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 차원에서 같이 고민해서 우리의 베네핏으로 만들면 되는 것 아니었을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 구축한 gitlab 서버는 현재 &lt;u&gt;옆팀&lt;/u&gt;에서 아주 잘~ 쓰고 있다고 전해들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 일화들이 쌓여가며 성장이 어려운 환경이라는 생각은 확고해졌고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부의 개발자들에 비해 뒤쳐진 일을 하고 있다는 위기감도 점점 커지고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 솔루션의 한계에서 오는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽을 다뤄볼 기회가 전혀 없었다. 트래픽이 없으면 운영 경험을 쌓기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내는 200명 남짓이고 사내망에서만 동작하는 솔루션이다보니 TPS가 10 이상으로 치솟는 일 자체가 드문 환경이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 꼭 B2C IT 서비스를 하는 회사에서 대용량 트래픽에 얻어맞아가며 전문적인 경험도 쌓고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 문화가 좋은 곳에서 서로가 서로의 성장을 견인하는 곳으로 가자고 결심했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이직 준비&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직 준비야 꽤 오래하긴 했지만, 이번에 성공할 수 있었던 가장 중요한 요소를 꼽아보면 &lt;b&gt;포트폴리오&lt;/b&gt;와 &lt;b&gt;기초 다지기&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트폴리오는 내 블로그에서 가장 조회수를 잘 벌어다주는 포스트인&amp;nbsp;&lt;a href=&quot;https://private-space.tistory.com/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;여기&lt;/b&gt;&lt;/a&gt;에 정리한 것처럼 만들었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 사용하지는 않아도 주요 서비스업에서 많이 사용하는 것들과 기초 위주로 공부했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 정리한 것은 아니지만 정말 잊고 싶지 않은 내용은 정리하여 블로그에 업로드했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전처럼 사용법만 익히려는 마음가짐으로 공부하진 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 스프링의 내부적인 동작 구조부터 공부하려고 하지 않았을까? 하는 실망에서부터 시작했기 때문인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크를 구성하는 코드를 파헤쳐가며 뇌리에 새기려고 노력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연한 사실이지만 이직을 위해서만 공부해야 할 내용들은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 공부했던 내용들이 개발에 관한 전반적인 이해도를 크게 높여주며 업무에도 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 직장에서도, 현재 직장에서도 그렇게 느끼고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;지원서 제출&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머스 잡 매칭은 한 번의 코딩 테스트로 최대 5개의 회사에 지원할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 개는 보자마자 골랐고 나머지 2개는 열심히 골라서 괜찮은 곳으로 채워넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 다니는 회사는 보자마자 고른 곳 중 하나인데, 작년 말에도 도전했고 이번에도 도전했을 정도로 정말 원했던 회사 중 하나이기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 했던 업무를 정리하고 자기소개 양식에 맞춰서 내용을 작성하여 지원을 완료했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트는 평소에도 준비를 해두었기 때문에 별 문제없이 모든 문제를 맞출 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경력직 코테는 어딜가나 그리 어렵진 않다. 수치로 따져보면 프로그래머스 레벨 3정도?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백준이든 프로그래머스든 상관없이 탈출을 희망한다면 꾸준히 문제를 풀어나가는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 결과는 코딩테스트가 종료되고 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개는 광탈했고 3개의 회사에 면접 일정이 잡혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;면접&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트에 자신이 있었던 만큼 서류 결과가 나오기 전부터 면접을 준비했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CS와 자소서 내용 위주로 대비를 했는데, 아무래도 운영체제쪽은 다시 복기할 필요가 있다고 느껴서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 정리했던 내용들을 다시 훑어보았다. (면접 문제로 깊게까지 물어보니 굉장히 어려웠다...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시국이 시국이다보니 3개 회사 모두 비대면 면접으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 회사는 화면과 얼굴 공유를 모두 켜고 라이브 코딩 테스트를 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;비대면은 &lt;u&gt;&lt;b&gt;컴퓨터 성능&lt;/b&gt;&lt;/u&gt;도 굉장히 중요하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 때 사용했던 내 노트북이 2018년형 맥북 에어였는데, 조금만 무리해도 스로틀링에 걸려 정신을 못차린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브 코딩 1시간, 리뷰 1시간 순으로 진행한 면접이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롬과 카메라, 화면공유, 인텔리제이까지 켜고 라이브 코딩을 시작했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥북이 미친듯이 열을 내뿜었고 방은 열기로 가득찼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 타이핑까지 렉이 걸렸다. 코딩 자체를 할 수가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 면접에서 탈락해버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 두 개의 회사는 화상으로 질문과 답변만 오갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;라이브 코딩보단 낫지만 계속 연결이 툭툭 끊겨서 면접관이 말이 끊겨 들렸던 문제가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 질문을 들어보니 블로그를 작성하고 깊게까지 공부하려던 노력이 유효했음을 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 질문은 잘 기억이 안나는데 스프링의 내부 구조를 많이 물어본 점은 기억한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 면접에서는&amp;nbsp;임원분과 1:1로 면접을 보았으나 선배 개발자와 이야기하듯 즐거웠었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;내가 회사에서 많이 고민했던 점, 풀어나가려고 노력한 점, &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그리고 동료들과 하고 싶었던 것들에 대해 대화를 나눴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;현재 환경에서 많은 고민을 한게 느껴진다&quot;고 공감을 해주셔서 그런지 편안했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 원하던 회사에 합격을 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;현재 당신은 어떤가요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 문제로 고민하는 사람은 꽤 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직이나 이직 사유는 개발자 단톡방 등 커뮤니티에서도 자주 보이는 주제이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직 사유가 없어도 직장인이라면 누구나 가슴 속에 퇴직서는 품고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직은 분명 두렵기도 하고 감정소모가 많이 되는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 내가 꽉 잡고 있는 환경이 앞으로 살아가야 할 시간에 도움이 되지 않는다면 오래 잡고 있는게 오히려 손해다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 가는 환경이 안좋다면 더 큰 손해다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돈 많이 주는 곳, 복지 좋은 곳과 같은 기준도 좋지만 그렇게 이직해서 본인에게 행복할지는 심사숙고를 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;누구나 같은 시간을 살아가는 만큼 시간이라는 기회비용은 그 무엇보다도 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;새 회사 생활&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연봉 협상을 잘 하지 못해서 금액은 영 눈물이 나지만...  &lt;span style=&quot;color: #333333;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그 점 하나만 빼면 기대만큼이나 만족스러운 생활을 하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 문화도 맘에 들고 모두가 참여하는 팀내 스터디도 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 경험이 부족하여 수습을 통과하지 못할거란 불안감도 있었으나 무사히 3개월이 지나갔고 프로그래머스 잡 매칭 사이닝보너스도 받았다.&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 회사에 다니는 내내 이직을 준비하다가 감정을 너무 많이 소모했는지 번아웃이 제대로 왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 너무 만족스러운 것과는 별개로&amp;nbsp;공부가 손에 거의 잡히지 않고 아무 것도 하기 싫었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부끄러운 일이다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 더 열심히, 의욕적으로 해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 할지는 조금 더 고민을 해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전처럼 블로그에 집중하고 싶은데 책읽고 정리한 것보다는 나만의 컨텐츠를 쓰고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 직접 작성한 포스트의 조회수가 잘 나오기도 하고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그저 누군가가 정해놓은 뒤꽁무니만 쫓는 사람이 되고 싶지는 않기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년은 이직을 통해 한 걸음 성장하는 계기를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인생에 있어서 큰 발자취가 아닐까 하고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직을 시도하는 모두가 힘을 내어 원하는 바를 이루었으면 한다.&amp;nbsp; &lt;/p&gt;</description>
      <category>기타/회고</category>
      <category>이직</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/111</guid>
      <comments>https://private-space.tistory.com/111#entry111comment</comments>
      <pubDate>Tue, 26 Jan 2021 01:35:25 +0900</pubDate>
    </item>
    <item>
      <title>안전하게 코딩하는 좋은 습관, 일급 콜렉션</title>
      <link>https://private-space.tistory.com/110</link>
      <description>&lt;h1&gt;일급 콜렉션&lt;/h1&gt;
&lt;p&gt;일급 콜렉션은 콜렉션 외의 필드가 존재하지 않는 클래스를 의미한다.&lt;/p&gt;
&lt;h2&gt;왜 사용하는가&lt;/h2&gt;
&lt;p&gt;콜렉션을 필드로 사용하는 도메인 객체는 데이터 일관성에 관한 문제가 생길 가능성이 존재한다.&lt;/p&gt;
&lt;p&gt;다음의 예시를 살펴보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@AllArgsConstructor
public class Order {
    private final long orderId;
    private final String name;
    private final List&amp;lt;LocalDateTime&amp;gt; orderTimes;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void order() {
    List&amp;lt;LocalDateTime&amp;gt; orderTimes = new ArrayList&amp;lt;&amp;gt;();
    orderTimes.add(LocalDateTime.now());
    orderTimes.add(LocalDateTime.now());

    Order order = new Order(0, &amp;quot;NAME&amp;quot;, orderTimes);
    orderTimes.add(LocalDateTime.now());
    // order.orderTimes의 크기는 2? 3?
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;비록 &lt;code&gt;Order&lt;/code&gt; 의 모든 필드가 final으로 불변임을 보장하지만 orderTimes에 대해서는 약간 다르다.&lt;/p&gt;
&lt;p&gt;final은 그저 무조건 한 번 할당하게 한다는 의미로, &lt;code&gt;List&amp;lt;LocalDateTime&amp;gt;&lt;/code&gt; 인스턴스의 주소값을 가질 뿐이기 때문이다.&lt;/p&gt;
&lt;p&gt;따라서 &lt;code&gt;Order&lt;/code&gt; 인스턴스 생성 뒤 orderTimes에 데이터를 더 넣어주면 바로 반영되는 것이다.&lt;/p&gt;
&lt;h2&gt;데이터 불일치 해결하기&lt;/h2&gt;
&lt;p&gt;일급 콜렉션을 사용하여 데이터의 정합성을 보장할 수 있도록 변경해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class OrderTimes {

    private final List&amp;lt;LocalDateTime&amp;gt; times;

    public OrderTimes(List&amp;lt;LocalDateTime&amp;gt; times) {
        this.times = new ArrayList&amp;lt;&amp;gt;(times);
    }

    public List&amp;lt;LocalDateTime&amp;gt; getTimes() {
        return Collections.unmodifiableList(times);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@AllArgsConstructor
public class Order {
    private final long orderId;
    private final String name;
    private final OrderTimes orderTimes;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;OrderTimes&lt;/code&gt; 라는 클래스를 새로 만들었다.&lt;/p&gt;
&lt;p&gt;그리고 생성 시점에서 새로운 &lt;code&gt;ArrayList&lt;/code&gt; 를 할당하여 필드로 사용하는 것을 강제했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getTimes()&lt;/code&gt; 메소드에서는 변경 불가능한 리스트로 반환한다.&lt;/p&gt;
&lt;p&gt;이렇게 되면 사용자는 &lt;code&gt;OrderTimes&lt;/code&gt; 를 생성한 뒤 데이터를 직접 통제할 수 없다.&lt;/p&gt;
&lt;h3&gt;주의점&lt;/h3&gt;
&lt;p&gt;여기서도 주의할 점이 하나 있는데, 생성자를 다시 한번 살펴보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 원래 코드
public OrderTimes(List&amp;lt;LocalDateTime&amp;gt; times) {
    this.times = new ArrayList&amp;lt;&amp;gt;(times);
}

// 만약 이렇다면?
public OrderTimes(List&amp;lt;LocalDateTime&amp;gt; times) {
    this.times = times;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;매개변수가 &lt;code&gt;List&lt;/code&gt; 타입으로 들어옴에도 불구하고 this.times에 새로운 &lt;code&gt;ArrayList&lt;/code&gt; 를 할당한 뒤 기존 데이터를 복사하여 넘겨주었다.&lt;/p&gt;
&lt;p&gt;복사하여 새로 할당하지 않으면 &lt;code&gt;List&amp;lt;LocalDateTime&amp;gt;&lt;/code&gt; 의 인스턴스로 &lt;code&gt;OrderTimes&lt;/code&gt; 를 통제할 수 있게 되니 꼭 새로운 인스턴스로 복사하여 넘겨주는 것이 안전하다.&lt;/p&gt;
&lt;h2&gt;Collections.unmodifiableList&lt;/h2&gt;
&lt;p&gt;이 메소드는 자바 내장 라이브러리에서 제공하고 있는 메소드로,&lt;/p&gt;
&lt;p&gt;이 메소드를 통해 반환된 콜렉션은 데이터를 변경할 수 없다는 특징을 갖고 있다.&lt;/p&gt;
&lt;p&gt;즉 원본을 가지지만 변경할 수 없도록 통제하는 프록시인 것이다.&lt;/p&gt;
&lt;p&gt;라이브러리 코드를 파헤쳐서 들어가보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;static class UnmodifiableList&amp;lt;E&amp;gt; extends Collections.UnmodifiableCollection&amp;lt;E&amp;gt; 
    implements List&amp;lt;E&amp;gt; {

    private static final long serialVersionUID = -283967356065247728L;
    final List&amp;lt;? extends E&amp;gt; list;

    UnmodifiableList(List&amp;lt;? extends E&amp;gt; list) {
        super(list);
        this.list = list;
    }

    ...

    public E get(int index) {
        return this.list.get(index);
    }

    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;생성자를 보면 원본을 그대로 가져와 필드에 할당한다.&lt;/p&gt;
&lt;p&gt;get 메소드는 원본 콜렉션과 기능이 같다.&lt;/p&gt;
&lt;p&gt;데이터를 변환하는 set, add remove는 예외를 발생시켜 변경할 수 없도록 강제한다.&lt;/p&gt;
&lt;p&gt;만약 &lt;code&gt;getTimes()&lt;/code&gt; 호출 후 데이터를 적절하게 변경해야 하지만 원본은 유지하고 싶다면 복사해서 넘겨주는 편이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public List&amp;lt;LocalDateTime&amp;gt; getTimes() {
    return List.copyOf(times);
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Java</category>
      <category>시큐어코딩</category>
      <category>일급콜렉션</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/110</guid>
      <comments>https://private-space.tistory.com/110#entry110comment</comments>
      <pubDate>Sun, 8 Nov 2020 22:21:27 +0900</pubDate>
    </item>
    <item>
      <title>멱등성과 테스트</title>
      <link>https://private-space.tistory.com/109</link>
      <description>&lt;h1&gt;멱등성과 테스트의 관계&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드 내부에서 사용하기만 해도 테스트가 어려워지는 유형의 코드가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 예제를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class DifficultTest {

    public int nextLottoNumber() {
        Random random = new Random();
        return random.nextInt(45) + 1;
    }

    public String todayDateToString() {
        return LocalDateTime.now().toString();
    }

    public String inputIntToString() {
        Scanner scanner = new Scanner(System.in);

        int a = scanner.nextInt();
        int b = scanner.nextInt();

        return String.valueOf(a) + String.valueOf(b);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드 하나가 하나의 예시라고 생각하고 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통점은 &lt;code&gt;멱등성&lt;/code&gt; 이 지켜지지 않는다는 코드라는 점에 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트가 어려운 이유?  &lt;/h2&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;nextLottoNumber&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로또 번호를 반환하는 메소드이다.&lt;/li&gt;
&lt;li&gt;nextInt()는 호출 시마다 다른 값이 반환되며 여럿 호출한다 해도 연속으로 같은 값이 나오리라는 보장이 없다.&lt;/li&gt;
&lt;li&gt;기대 입력값은 없지만 기대 출력값은 범위 내 임의의 숫자로, 입출력이 1:1 매칭되지 않는다.&lt;/li&gt;
&lt;li&gt;메소드 내에서 &lt;code&gt;Random&lt;/code&gt;을 생성하고 사용한다. 이 메소드에 숫자 생성 전략이 강하게 결합되어 있어 분리하기 쉽지 않다는 문제도 가지고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;todayDateToString&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;LocalDateTime.now()&lt;/code&gt; 는 자바 8에서 도입된 이래로 현재 시간을 대입하는 용도로 많이 사용되고 있다.&lt;/li&gt;
&lt;li&gt;메소드 호출 시점에 따라 값이 달라지며 어떤 값이 결과로 나타날지 정확하게 테스트할 수는 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inputIntToString&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 개의 int값을 입력받고 문자열로 합쳐 반환하는 메소드이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Scanner&lt;/code&gt; 표준 입력의 특성 상 입력받을 데이터가 많아지면 테스트하기가 어려워진다.&lt;/li&gt;
&lt;li&gt;같은 값을 계속 입력하면 멱등성이 아닌가 하는 생각이 들 수도 있지만, 이 코드가 사용되는 모든 케이스에서 같은 값이 계속 입력하기를 기대하기는 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등성으로 인해 테스트가 어려워졌다면, 멱등성을 보장하면 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import java.time.Clock;
import java.time.LocalDateTime;

public class DifficultTest {

    public int nextLottoNumber(LottoStrategy lottoStrategy) {
        return lottoStrategy.nextNumber();
    }

    // type 1
    public String todayDateToString1(LocalDateTime localDateTime) {
        return localDateTime.toString();
    }

    // type 2
    public String todayDateToString2(Clock clock) {
        return LocalDateTime.now(clock).toString();
    }

    public String inputIntToString(int a, int b) {
        return String.valueOf(a) + String.valueOf(b);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface LottoStrategy {
    int nextNumber();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;nextLottoNumber&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;번호 생성의 책임을 &lt;code&gt;LottoStrategy&lt;/code&gt; 으로 옮겼다.&lt;/li&gt;
&lt;li&gt;이제 이 메소드는 다음 번호를 뽑아낸다는 하나의 책임만을 가진다.&lt;/li&gt;
&lt;li&gt;테스트를 할 때도 &lt;code&gt;LottoStrategy&lt;/code&gt; 를 정의해서 테스트하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test1() {
  DifficultTest difficultTest = new DifficultTest();

  int nextNumber = 3;
  assertEquals(difficultTest.nextLottoNumber(() -&amp;gt; nextNumber), nextNumber);
}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;todayDateToString&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;LocalDateTime&lt;/code&gt; 을 메소드로 주입받거나, &lt;code&gt;Clock&lt;/code&gt; 을 주입받아서 해결할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LocalDateTime&lt;/code&gt; 을 직접 주입받으면 입출력 값을 정확하게 통제할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Clock&lt;/code&gt; 은 now()의 매개변수로 삽입하여 시점을 통제한다.&amp;nbsp;&lt;br /&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;@Test 
public void test2() { 
    DifficultTest difficultTest = new DifficultTest();    
    Clock clock = Clock.systemDefaultZone();
    
    System.out.println(difficultTest.todayDateToString2(clock)); 
    System.out.println(difficultTest.todayDateToString2(clock)); 
    System.out.println(difficultTest.todayDateToString2(clock)); 
 }
 
 /* 실행 결과
    2020-11-08T21:36:18.795481200
    2020-11-08T21:36:18.795481200
    2020-11-08T21:36:18.795481200
  */&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inputIntToString&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;scanner.nextInt()&lt;/code&gt; 를 mock하지 않고 주입받도록 변경했다.&lt;/li&gt;
&lt;li&gt;테스트하기가 매우 편리해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;멱등성이 지켜지는 코드&lt;/b&gt;는 테스트하기가 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 뭐 그까짓거 안해도 되지 않냐? 라고 생각할 수 있는데, 코드를 테스트하기 쉬우면 적절하게 책임을 잘 분리했다는 말도 된다.&lt;br /&gt;유지보수에도 도움이 많이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐만 아니라 멱등성은 API를 설계할 때도 고려하기에 좋은 요소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등성이 지켜진다면 예측할 수 없는 다수의 요청이 한 번에 들어오는 상황에도 잘 대처할 수 있기 때문이다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>멱등성</category>
      <category>테스트</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/109</guid>
      <comments>https://private-space.tistory.com/109#entry109comment</comments>
      <pubDate>Sun, 8 Nov 2020 21:45:42 +0900</pubDate>
    </item>
    <item>
      <title>구글에서 검색할 때 검색 결과에서 특정 사이트 제외하기</title>
      <link>https://private-space.tistory.com/108</link>
      <description>&lt;p&gt;구글링을 하다보면 보고 싶지 않은데 계속 검색되는 사이트가 있다.&lt;/p&gt;
&lt;p&gt;나같은 경우는 모 티스토리 블로그를 굉장히 싫어한다.&lt;/p&gt;
&lt;p&gt;stackoverflow의 글을 크롤링하여 번역기를 돌려 올리는 블로그인데, 검색했다 하면 이 블로그가 튀어나온다.&lt;/p&gt;
&lt;p&gt;번역기는 돌렸지만 번역기가 아직은 완벽하지 않기 때문에 원문을 봐야 하고 url 타고 들어가야만 확인이 가능하다.&lt;/p&gt;
&lt;p&gt;모은 데이터가 한두 건이 아닌지라 정말 많이 보이다보니 검색 품질이 이렇게도 나빠지는구나 싶었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;크롬 익스텐션으로 간단하게 해결할 수 있다. 크로미움 기반의 브라우저에서도 사용 가능하다.&lt;/p&gt;
&lt;p&gt;아래 링크로 들어가서 다운받고 브라우저에 추가하자.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/personal-blocklistnot-by/cbbbhelcpfjhdcncigdlkabmjbgokmpg/related&quot;&gt;https://chrome.google.com/webstore/detail/personal-blocklistnot-by/cbbbhelcpfjhdcncigdlkabmjbgokmpg/related&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1598344649546&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Personal Blocklist(not by Google)&quot; data-og-description=&quot;Block Website from Google search results&quot; data-og-host=&quot;chrome.google.com&quot; data-og-source-url=&quot;https://chrome.google.com/webstore/detail/personal-blocklistnot-by/cbbbhelcpfjhdcncigdlkabmjbgokmpg/related&quot; data-og-url=&quot;https://chrome.google.com/webstore/detail/personal-blocklistnot-by/cbbbhelcpfjhdcncigdlkabmjbgokmpg&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ba0hRy/hyHgBj7cSO/mt8bpeK0xm94Yk7Vh2QaZ1/img.png?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128&quot;&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/personal-blocklistnot-by/cbbbhelcpfjhdcncigdlkabmjbgokmpg/related&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chrome.google.com/webstore/detail/personal-blocklistnot-by/cbbbhelcpfjhdcncigdlkabmjbgokmpg/related&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ba0hRy/hyHgBj7cSO/mt8bpeK0xm94Yk7Vh2QaZ1/img.png?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Personal Blocklist(not by Google)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Block Website from Google search results&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;chrome.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사용법은 굉장히 쉽다.&lt;/p&gt;
&lt;p&gt;브라우저 상단의 빨간 아이콘을 누른다.&lt;/p&gt;
&lt;p&gt;아래 사진의 하얀 사각형 안에 있는 아이콘이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nuqq6/btqHgqDKSTv/wNgRG3zy8BpsZerNgbHE2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nuqq6/btqHgqDKSTv/wNgRG3zy8BpsZerNgbHE2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nuqq6/btqHgqDKSTv/wNgRG3zy8BpsZerNgbHE2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnuqq6%2FbtqHgqDKSTv%2FwNgRG3zy8BpsZerNgbHE2K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 Import를 눌러 보기 싫은 도메인을 추가하자.&lt;/p&gt;
&lt;p&gt;앞으로 구글에서 해당 도메인이 검색되는 일은 없다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>검색</category>
      <category>구글링</category>
      <category>차단</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/108</guid>
      <comments>https://private-space.tistory.com/108#entry108comment</comments>
      <pubDate>Tue, 25 Aug 2020 17:41:29 +0900</pubDate>
    </item>
    <item>
      <title>Mac에서 사용 중인 포트 쉽게 확인하는 방법</title>
      <link>https://private-space.tistory.com/107</link>
      <description>&lt;h1&gt;Mac에서 사용 중인 포트 쉽게 확인하기&lt;/h1&gt;
&lt;p&gt;개발을 하다 보면 사용 중인 포트를 확인해야 할 때가 꽤 많다.&lt;/p&gt;
&lt;p&gt;맥에서는 netstat이 아니라 lsof를 사용하여 조회한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo lsof -nP -i4TCP:{PORT} | grep LISTEN&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;매번 다 치기는 귀찮다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;zsh function&lt;/code&gt;으로 등록해서 사용해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 에디터로 .zshrc 편집 시작
vim ~/.zshrc  (bash인 경우 ~/.bashrc)

# 아래 내용을 기입
port() {
    sudo lsof -nP -i4TCP:$1 | grep LISTEN
}

# 적용
source ~/.zshrc

# 사용
port 80&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;zshrc 파일에 등록했으므로 쉘을 껐다 닫아도 옵션이 유지된다.&lt;/p&gt;</description>
      <category>기타</category>
      <category>bash</category>
      <category>Mac</category>
      <category>zsh</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/107</guid>
      <comments>https://private-space.tistory.com/107#entry107comment</comments>
      <pubDate>Mon, 17 Aug 2020 17:31:27 +0900</pubDate>
    </item>
    <item>
      <title>Java Stream (6) 스트림 생성</title>
      <link>https://private-space.tistory.com/106</link>
      <description>&lt;h1&gt;스트림 생성&lt;/h1&gt;
&lt;p&gt;다양한 방식으로 스트림을 생성할 수 있다.&lt;/p&gt;
&lt;h2&gt;빈 스트림 생성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Stream::empty&lt;/code&gt; 는 빈 스트림을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream&amp;lt;String&amp;gt; emptyStream = Stream.empty();&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;매개변수를 사용한 생성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Stream::of&lt;/code&gt; 는 매개변수 목록을 받고 &lt;code&gt;Arrays.stream&lt;/code&gt; 을 반환하여 스트림을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream&amp;lt;String&amp;gt; cityNamesStream = Stream.of(&amp;quot;Seoul&amp;quot;, &amp;quot;Incheon&amp;quot;,
        &amp;quot;Ulsan&amp;quot;, &amp;quot;Daegu&amp;quot;, &amp;quot;Gwangju&amp;quot;, &amp;quot;Busan&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;null이 될 수도 있는 객체를 스트림화&lt;/h2&gt;
&lt;p&gt;Java 9 이상에서 추가된 &lt;code&gt;Stream.ofNullable&lt;/code&gt; 은 매개변수의 값이 null이 아니면 스트림을 생성한다.&lt;/p&gt;
&lt;p&gt;null인 경우 &lt;code&gt;Stream.empty&lt;/code&gt; 를 반환하여 빈 스트림을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
map.put(&amp;quot;metropolitan&amp;quot;, Arrays.asList(&amp;quot;Seoul&amp;quot;, &amp;quot;Incheon&amp;quot;));
map.put(&amp;quot;Jeolla&amp;quot;, Arrays.asList(&amp;quot;Gwangju&amp;quot;));
map.put(&amp;quot;Gyeongsang&amp;quot;, Arrays.asList(&amp;quot;Ulsan&amp;quot;, &amp;quot;Daegu&amp;quot;, &amp;quot;Busan&amp;quot;));

Stream&amp;lt;String&amp;gt; cityNamesStream =
        Stream.ofNullable(map.get(&amp;quot;metropolitan&amp;quot;).stream().collect(Collectors.joining()));
cityNamesStream.forEach(System.out::println); // SeoulIncheon

cityNamesStream = Stream.ofNullable(map.get(&amp;quot;Gangwon&amp;quot;).stream().collect(Collectors.joining()));
cityNamesStream.forEach(System.out::println); // 빈 스트림&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;배열으로부터 스트림 생성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Arrays.stream&lt;/code&gt; 은 배열을 매개변수로 받아서 스트림을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String[] cities = { &amp;quot;Seoul&amp;quot;, &amp;quot;Incheon&amp;quot;, &amp;quot;Gwangju&amp;quot;, &amp;quot;Ulsan&amp;quot;, &amp;quot;Daegu&amp;quot;, &amp;quot;Busan&amp;quot; };
Stream&amp;lt;String&amp;gt; stream = Arrays.stream(cities);
stream.forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;파일으로부터 스트림 생성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Files::lines&lt;/code&gt; 는 파일을 읽어 행을 하나의 &lt;code&gt;String&lt;/code&gt;으로 간주하는 스트림을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;try {
    Stream&amp;lt;String&amp;gt; fileStream = Files.lines(Paths.get(&amp;quot;test.txt&amp;quot;), Charset.defaultCharset());
    fileStream.flatMap(line -&amp;gt; Arrays.stream(line.split(&amp;quot; &amp;quot;)));
    fileStream.forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;함수형 인터페이스로 스트림 생성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Stream::iterate&lt;/code&gt; 와 &lt;code&gt;Stream::generate&lt;/code&gt; 는 함수형 인터페이스를 매개변수로 받아 무한 스트림을 생성할 수 있다.&lt;/p&gt;
&lt;h3&gt;iterate&lt;/h3&gt;
&lt;p&gt;iterate는 for문과 비슷한 구조를 갖는다.&lt;/p&gt;
&lt;p&gt;매개변수는 초기값, &lt;code&gt;Predicate&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;UnaryOperator&amp;lt;T&amp;gt;&lt;/code&gt; 를 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Predicate&amp;lt;T&amp;gt;&lt;/code&gt; 는 선택사항이기 때문에 없어도 동작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream.iterate(0, n -&amp;gt; n + 1) // 초기값은 0, n은 1씩 증가
        .limit(5)
        .forEach(System.out::println);

Stream.iterate(0, n -&amp;gt; n &amp;lt; 5, n -&amp;gt; n + 1) // 위와 같으나 n &amp;lt; 5 조건 추가
        .forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;generate&lt;/h3&gt;
&lt;p&gt;매개변수로 &lt;code&gt;Supplier&amp;lt;T&amp;gt;&lt;/code&gt; 만 받기 때문에 스트림 생성 시 별도의 인수가 필요없는 경우에만 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 로또 번호를 생성해보자.
Random random = new Random();
IntStream.generate(() -&amp;gt; random.nextInt(45) + 1) // IntStream 역시 생성할 수 있다.
        .limit(6)
        .forEach(System.out::println);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Java</category>
      <category>Java</category>
      <category>Stream</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/106</guid>
      <comments>https://private-space.tistory.com/106#entry106comment</comments>
      <pubDate>Wed, 22 Jul 2020 01:52:54 +0900</pubDate>
    </item>
    <item>
      <title>Java Stream (5) 기본 자료형 스트림</title>
      <link>https://private-space.tistory.com/105</link>
      <description>&lt;h1&gt;기본 자료형 스트림&lt;/h1&gt;
&lt;p&gt;이번 포스트에서도 아래의 예시 데이터를 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Getter
@AllArgsConstructor
@EqualsAndHashCode
public class City {
    private String name;
    private double area;
    private int population;
    private String areaCode;
}

List&amp;lt;City&amp;gt; cities = Arrays.asList(
        new City(&amp;quot;Seoul&amp;quot;, 605.2, 9720846, &amp;quot;02&amp;quot;),
        new City(&amp;quot;Incheon&amp;quot;, 1063.3, 2947217, &amp;quot;032&amp;quot;),
        new City(&amp;quot;Ulsan&amp;quot;, 1062, 1142190, &amp;quot;052&amp;quot;),
        new City(&amp;quot;Daegu&amp;quot;, 883.5, 2427954, &amp;quot;053&amp;quot;),
        new City(&amp;quot;Gwangju&amp;quot;, 501.1, 1455048, &amp;quot;062&amp;quot;),
        new City(&amp;quot;Busan&amp;quot;, 770.1, 3404423, &amp;quot;051&amp;quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;일반 스트림을 사용하여 cities의 모든 population을 구해보면 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int sum = cities.stream()
        .map(City::getPopulation)
        .reduce(0, Integer::sum);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드가 작동하는 데엔 문제는 없지만 내부적으로 박싱/언박싱을 수행하므로 성능이 크게 저하된다.&lt;/p&gt;
&lt;h2&gt;개념&lt;/h2&gt;
&lt;p&gt;스트림은 &lt;code&gt;IntStream&lt;/code&gt; , &lt;code&gt;DoubleStream&lt;/code&gt; , &lt;code&gt;LongStream&lt;/code&gt; 세 가지가 존재한다.&lt;/p&gt;
&lt;p&gt;다만 일반 스트림에 비해 특별하게 추가된 기능은 없고 박싱/언박싱 관련 효율성의 장점을 갖는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int sum = cities.stream()
        .mapToInt(City::getPopulation) // IntStream으로 변환
        .sum();&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;변환 방법&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Stream&lt;/code&gt; 클래스에 포함된 세 가지 메소드를 사용한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Stream::mapToInt&lt;/code&gt; → &lt;code&gt;IntStream&lt;/code&gt; 반환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Stream::mapToDouble&lt;/code&gt; → &lt;code&gt;DoubleStream&lt;/code&gt; 반환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Stream::mapToLong&lt;/code&gt; → &lt;code&gt;LongStream&lt;/code&gt; 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;기본형 타입 스트림으로 바꾸었으니 반대로 일반형인 &lt;code&gt;Stream&amp;lt;T&amp;gt;&lt;/code&gt; 으로도 다시 변환할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;IntStream::boxed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DoubleStream::boxed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LongStream::boxed&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;메소드 명은 모두 &lt;strong&gt;boxed&lt;/strong&gt;로 같다.&lt;/p&gt;
&lt;h2&gt;Optional 활용&lt;/h2&gt;
&lt;p&gt;최대값 등 Nullable 가능성이 있는 최종 연산은 &lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt; 를 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional&amp;lt;Integer&amp;gt; max = cities.stream()
        .map(City::getPopulation)
        .reduce(Integer::max);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt; 는 레퍼런스 타입에서만 활용할 수 있다.&lt;/p&gt;
&lt;p&gt;그러나 기본형 타입은 null이 따로 존재하지 않는다.&lt;/p&gt;
&lt;p&gt;int형 계산을 끝마친 결과가 0이라고 가정한다.&lt;/p&gt;
&lt;p&gt;int의 기본값은 0인데, 결과도 0이라는 것이 계산을 안 했다는 말은 아니다.&lt;/p&gt;
&lt;p&gt;이런 애매한 상황을 위해 기본형 타입에서도 사용할 수 있는 optional 클래스가 존재한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;OptionalInt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OptionalDouble&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OptionalLong&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;OptionalInt max = cities.stream()
        .mapToInt(City::getPopulation)
        .max();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기본형 Optional도 &lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt; 와 마찬가지로 .isPresent(), .orElse() 등의 메소드를 갖는다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>Java</category>
      <category>Stream</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/105</guid>
      <comments>https://private-space.tistory.com/105#entry105comment</comments>
      <pubDate>Wed, 22 Jul 2020 01:03:42 +0900</pubDate>
    </item>
    <item>
      <title>2020 상반기 회고</title>
      <link>https://private-space.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;올 해를 시작하며 작년의 회고록을 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 때 세워두었던 목표부터 점검하고 상반기엔 무엇을 이루었는지를 돌아보는 시간을 갖고자 이 글을 작성한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  올 해의 목표&amp;nbsp;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;블로그 활성화하기&lt;/li&gt;
&lt;li&gt;개인 서비스 만들기&lt;/li&gt;
&lt;li&gt;오픈소스 커뮤니티 활동&lt;/li&gt;
&lt;li&gt;(이직하기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로그 활성화하기&lt;/h3&gt;
&lt;p&gt;가장 잘 이룬 목표이면서도 사실상 이 것만 했다...&lt;/p&gt;
&lt;p&gt;이 글 포함 93개의 포스팅 중 31개가 2017년에 작성되었다.&lt;/p&gt;
&lt;p&gt;2019년 말 블로그를 다시 활성화시키겠다고 한 이후 62개를 작성했으니 약 3~4일에 한 번 작성한 셈이다.&lt;/p&gt;
&lt;p&gt;사실 조금 더 열심히 할 수 있었는데 이직한다고 코딩테스트 공부하고, 면접 공부한다는 핑계로 조금 소홀하기도 했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방문자 수 변화&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/efI3lc/btqFNOMuatq/E8kxNDpECSCfEuhzjTkaA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/efI3lc/btqFNOMuatq/E8kxNDpECSCfEuhzjTkaA0/img.png&quot; data-alt=&quot;2019년 초&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efI3lc/btqFNOMuatq/E8kxNDpECSCfEuhzjTkaA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FefI3lc%2FbtqFNOMuatq%2FE8kxNDpECSCfEuhzjTkaA0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2019년 초&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U0lIr/btqFNfRzWIt/B1qN6NszkFFCMDkndBkUrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U0lIr/btqFNfRzWIt/B1qN6NszkFFCMDkndBkUrK/img.png&quot; data-alt=&quot;현재&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U0lIr/btqFNfRzWIt/B1qN6NszkFFCMDkndBkUrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU0lIr%2FbtqFNfRzWIt%2FB1qN6NszkFFCMDkndBkUrK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;나름대로 글에 성의를 담아 작성하기 시작한 결과 방문자 수가 점점 증가하고 있다.&lt;/p&gt;
&lt;p&gt;많은 수치는 아니지만, 꾸준히 증가하는 수치가 내게는 소소한 즐거움이 되고 있다.&lt;/p&gt;
&lt;p&gt;주객전도인가 싶기도 하지만 포스팅을 위해 공부하는 습관을 들이기도 했다.&lt;/p&gt;
&lt;p&gt;어찌되었던 긍정적인 변화인 건 확실하다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개인 서비스 만들기/오픈소스 커뮤니티 활동&lt;/h3&gt;
&lt;p&gt;공부할 것이 산더미라 아직 건드리지도 못했다. (사실 핑계다⊙_⊙;)&lt;/p&gt;
&lt;p&gt;이직도 끝났겠다, 이제 개인 공부 빼면 바쁠 일이 없을테니 하반기엔 꼭 해보려고 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(이직하기)&lt;/h3&gt;
&lt;p&gt;회고록에 쓴 목표는 아니지만 올 해에 이직을 해보자는 계획은 있었다.&lt;/p&gt;
&lt;p&gt;불투명한 미래이자 목표이기에 적지 않았을 뿐이다.&lt;/p&gt;
&lt;p&gt;여튼 이직을 시도했고 성공했다!&lt;/p&gt;
&lt;p&gt;너무 가고 싶던 회사였는데 붙여줘서 너무 감사할 따름이다.  &lt;/p&gt;
&lt;p&gt;업무 분야는 웹 서비스가 되었다.&lt;/p&gt;
&lt;p&gt;기존이 B2B 솔루션이고 도메인이 바뀌는 만큼 업무 적응 진통이 예상된다.  &lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사실 올 해 목표를 다 제쳐버리고서라도 꼭 성공했으면 하는 목표였기 때문에 최근 굉장히 기쁜 나날을 보내고 있다.&lt;/p&gt;
&lt;p&gt;그 동안 주변 사람들에게 소홀했던 마음을 반성하며 모임을 자주 가지기도 했다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금은 전 직장에서 퇴사한 상태고 새 직장에 입사할 준비를 하고 있다.&lt;/p&gt;
&lt;p&gt;이직에 대한 내용은 추후 작성할 이직기에서 자세히 다뤄보고자 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기타&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올 해 삼성전자 B형 SW TEST가 나오면 시도해보려고 했으나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코로나로 인한 무기한 연기되었고 1월 단 한 번의 시험을 제외하곤 열리지 않고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년을 갈아 넣은 주제인데 한 번은 다시 시도해보고 싶었으나 현실이 이렇기에 더 이상 시도가 불가능한 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앞으로는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 일이었던 이직을 마쳤으니 올해 하려고 했던 것에 더 충실해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 직장에서 적응하는게 쉬울 것 같지도 않고 내가 하는 모든 일이 기존과는 다른 새로운 일이라는 생각도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올 해 들어서 공부에 돈을 아끼지 않으려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 올 초 책이나 인터넷 강의를 엄청 구매해 두었고, 아직도 보고 있다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;자바지기 박재성 님의 TDD 강의가 최근 오픈되었는데, 70만 원이라는 돈을 주고 수강 신청했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 직장에서 더 잘해보고 싶은 마음도 있고, 다른 개발자들은 어떻게 코드를 작성하는지 궁금한 마음이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 성장에 도움이 많이 되었다는 평이 대다수라는 점도 이 강의를 더 기대하게 하는 요소였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심이 있다면 &lt;a href=&quot;https://edu.nextstep.camp/c/8fWRxNWU&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;로...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 기록을 더 열심히 남기는 개발자가 되려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올 해는 이직이니 뭐니 하는 핑계로 더 많이 작성할 수 있던 포스팅을 게을리 했던 것도 있고,&lt;/p&gt;
&lt;p&gt;결국 내가 한 일은 기록과 기억으로밖에 되돌아 볼 수 없으니까...&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또 포스팅을 하다 보니 단순히 무언가를 공부하고 정리하기보단 &lt;b&gt;'나만의 컨텐츠를 작성해보자'&lt;/b&gt; 라는 것에 관심을 가지기 시작했다.&lt;/p&gt;
&lt;p&gt;실제로 블로그 조회수의 대부분은 어디에서도 참고하지 않고 작성한 나만의 포스트에서 나오고 있다.&lt;/p&gt;
&lt;p&gt;물론 남들의 기록을 다시 한번 내 식으로 정리하는 것도 좋다.&lt;/p&gt;
&lt;p&gt;그러나 나만의 컨텐츠를 잘 쓰는 것이 정말 글을 잘 쓰는 개발자란 생각이 들었다.&lt;/p&gt;
&lt;p&gt;앞으로는 '잘 정리하기' 보다 '나만의 관점에서 바라본' 글을 위해 고민해보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;큰 건도 하나 이루었으니 남은 목표까지 잘 이루는 한 해를 보내야겠다  &lt;/p&gt;</description>
      <category>기타/회고</category>
      <category>2020</category>
      <category>2020회고</category>
      <category>회고</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/104</guid>
      <comments>https://private-space.tistory.com/104#entry104comment</comments>
      <pubDate>Sat, 18 Jul 2020 13:55:25 +0900</pubDate>
    </item>
    <item>
      <title>Java Stream (4) 스트림 주요 메소드</title>
      <link>https://private-space.tistory.com/103</link>
      <description>&lt;h1&gt;스트림 주요 메소드&lt;/h1&gt;
&lt;p&gt;이번 포스팅에서는 스트림 활용 시 주로 사용되는 메소드를 다룬다.&lt;/p&gt;
&lt;p&gt;지난 포스팅과 비슷한 예제로 진행하였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Getter
@AllArgsConstructor
@EqualsAndHashCode    // equals, hashcode 자동 생성
public class City {
    private String name;
    private double area;
    private int population;
    private String areaCode;
}

List&amp;lt;City&amp;gt; cities = Arrays.asList(
        new City(&amp;quot;Seoul&amp;quot;, 605.2, 9720846, &amp;quot;02&amp;quot;),
        new City(&amp;quot;Incheon&amp;quot;, 1063.3, 2947217, &amp;quot;032&amp;quot;),
        new City(&amp;quot;Ulsan&amp;quot;, 1062, 1142190, &amp;quot;052&amp;quot;),
        new City(&amp;quot;Daegu&amp;quot;, 883.5, 2427954, &amp;quot;053&amp;quot;),
        new City(&amp;quot;Gwangju&amp;quot;, 501.1, 1455048, &amp;quot;062&amp;quot;),
        new City(&amp;quot;Busan&amp;quot;, 770.1, 3404423, &amp;quot;051&amp;quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;데이터 선별&lt;/h2&gt;
&lt;p&gt;조건에 따라 데이터를 선별하는 &lt;strong&gt;중간 연산&lt;/strong&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;cities.add(new City(&amp;quot;Seoul&amp;quot;, 0, 0, &amp;quot;02&amp;quot;));

List&amp;lt;City&amp;gt; streamNameList = cities.stream()
        .filter(city -&amp;gt; city.getArea() &amp;gt; 800) // ← 데이터 필터링
        .distinct()                           // ← 중복 제거
        .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Stream::filter&lt;/code&gt; 는 &lt;code&gt;Predicate&lt;/code&gt; 를 인자로 받는다.&lt;/p&gt;
&lt;p&gt;중복 요소를 제거하고 싶다면 &lt;code&gt;Stream::distinct&lt;/code&gt; 를 사용한다. (Seoul 하나가 삭제된다)&lt;/p&gt;
&lt;p&gt;중복을 판단하는 조건은 &lt;strong&gt;City 클래스에 오버라이드한 equals / hashcode&lt;/strong&gt;이다.&lt;/p&gt;
&lt;h2&gt;데이터 개수 조절&lt;/h2&gt;
&lt;p&gt;스트림 데이터를 자르거나 특정 요소만 선택한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;개수 제한&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::limit&lt;/code&gt; 로 개수를 조절할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  List&amp;lt;String&amp;gt; streamNameList = cities.stream()
          .filter(city -&amp;gt; city.getArea() &amp;gt; 800)
          .sorted(Comparator.comparing(City::getArea))
          .map(City::getName)
          .limit(2) // ← 최대 두 개의 원소만 반환한다.
          .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;건너뛰기&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::skip&lt;/code&gt; 으로 처음 n개를 건너뛸 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  cities.stream()
          .filter(city -&amp;gt; city.getArea() &amp;gt; 800)
          .sorted(Comparator.comparing(City::getArea))
          .map(City::getName)
          .skip(1) // ← 첫 번째 원소는 무시한다.
          .forEach(System.out::println);

  // 결과 (첫 번째 데이터 Daegu는 스킵)
  Ulsan
  Incheon&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;특정 조건에 부합하는 데이터만 선택&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;전제조건&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Java 9 이상에서 지원된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;데이터는 반드시 &lt;strong&gt;사용할 기준을 조건으로 정렬&lt;/strong&gt;되어 있는 상태여야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  // 예시 데이터를 area 오름차순으로 재가공
  List&amp;lt;City&amp;gt; orderByAreaList = cities.stream()
          .sorted(Comparator.comparing(City::getArea))
          .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::takeWhile&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스트림을 순회하다 조건이 false가 되는 순간 남은 데이터를 버린다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  orderByAreaList.stream()
          .takeWhile(city -&amp;gt; city.getArea() &amp;gt; 700)
          .map(City::getName)
          .forEach(System.out::println);

  // 결과
  Gwangju
  Seoul&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;데이터는 광주, 서울, 부산, 대구, 인천 순으로 정렬되어 있으나, 조건에 부합하지 않는 데이터인 부산부터 모든 데이터를 제거한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::dropWhile&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스트림을 순회하다 조건이 true가 되는 순간 지금까지 순회한 데이터를 모두 버린다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  orderByAreaList.stream()
          .dropWhile(city -&amp;gt; city.getArea() &amp;lt; 700)
          .map(City::getName)
          .forEach(System.out::println);

  // 결과
  Busan
  Daegu
  Ulsan
  Incheon&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::filter&lt;/code&gt; 와의 차이점&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Stream::filter&lt;/code&gt; 는 모든 원소를 검사하여 조건에 맞는 데이터만 선택하나, takeWhile, dropWhile은 데이터를 버리는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;데이터 변환&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;City&lt;/code&gt; 는 도시명, 면적, 인구수, 지역번호 데이터를 갖는 클래스이다.&lt;/p&gt;
&lt;p&gt;사용자의 목적에 따라 도시명 데이터만 사용해야 할 수도 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Stream::map&lt;/code&gt; 은 스트림 원소를 변환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;lt;String&amp;gt; filteringList = cities.stream()
        .filter(city -&amp;gt; city.getArea() &amp;gt; 800)
        .sorted(Comparator.comparing(City::getArea))
        .map(city -&amp;gt; city.getName()) // City 클래스에서 도시명만 사용하도록 한다.
        .collect(Collectors.toList());

// 결과 (첫 번째 데이터 Daegu는 스킵)
Ulsan
Incheon&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;단일 스트림으로 변환&lt;/h3&gt;
&lt;p&gt;만약 cities에서 지역 번호에 사용된 숫자에서 중복을 제거한 목록를 스트림으로 얻어내고 싶다면 어떻게 해야 할까?&lt;/p&gt;
&lt;p&gt;먼저 areaCode의 목록을 추출해본다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;lt;String&amp;gt; areaCodes = cities.stream()
        .map(City::getAreaCode)
        .collect(Collectors.toList());
// [&amp;quot;02&amp;quot;, &amp;quot;032&amp;quot;, &amp;quot;052&amp;quot;, &amp;quot;053&amp;quot;, &amp;quot;062&amp;quot;, &amp;quot;051&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;areaCodes를 문자 단위로 분해&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt; 0, 2
 0, 3, 2
 0, 5, 2
 0, 5, 3
 0, 6, 2
 0, 5, 1&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;areaCodes를 단일 스트림으로 변환&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt; 0, 2, 0, 3, 2, 0, 5, 2, 0, 5, 3, 0, 6, 2, 0, 5, 1&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;areaCodes를 스트림으로 추출하면 위의 1번처럼 문자열 하나가 데이터 하나로 변환된다.&lt;/p&gt;
&lt;p&gt;문자 단위로 분해하여 2와 같은 단일 문자열 스트림을 반환해야 한다.&lt;/p&gt;
&lt;p&gt;이럴 때 사용하는 것이 &lt;code&gt;Stream::flatMap&lt;/code&gt; 이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;areaCodes.stream()
        .map(areaCode -&amp;gt; areaCode.split(&amp;quot;&amp;quot;)) // areaCode를 문자 단위로 분해한 배열로 변환
        .flatMap(Arrays::stream)             // 변환된 스트림을 단일 스트림으로 변환
        .distinct()                          // 중복 제거
        .forEach(System.out::println);

// 결과
0
2
3
5
6
1&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;데이터 일치 여부&lt;/h2&gt;
&lt;p&gt;여기서 소개하는 메소드는 모두 boolean을 반환하는 최종 연산이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Short-circuit_evaluation&quot;&gt;Short circuit evaluation&lt;/a&gt;&lt;/strong&gt; 기법이 적용되어 값을 즉시 반환할 수 있는 상태가 되면 남은 원소를 확인하지 않는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Short circuit 예시&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  if (true || false) { ... }
  if (false &amp;amp;&amp;amp; true) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;두 식 모두 첫 번째 boolean만 평가하면 결과를 도출할 수 있다.&lt;/li&gt;
&lt;li&gt;따라서 두 번째 boolean은 확인하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::anyMatch&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;검색 조건에 맞는 원소가 1개 이상인 경우 true&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;boolean areaOver1000 = cities.stream()
      .anyMatch(city -&amp;gt; city.getArea() &amp;gt; 1000);

boolean areaOver2000 = cities.stream()
      .anyMatch(city -&amp;gt; city.getArea() &amp;gt; 2000);

System.out.println(areaOver1000);
System.out.println(areaOver2000);

// 결과
true
false&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::allMatch&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;모든 원소가 검색 조건에 일치해야 true&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;boolean allAreasOver100 = cities.stream()
      .allMatch(city -&amp;gt; city.getArea() &amp;gt; 100);

boolean allAreasOver1000 = cities.stream()
      .allMatch(city -&amp;gt; city.getArea() &amp;gt; 1000);

System.out.println(allAreasOver100);
System.out.println(allAreasOver1000);

// 결과
true
false&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::nonMatch&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;모든 원소가 검색 조건에 일치하지 않아야 true (allMatch와 반대 동작)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;boolean allAreasAreNotOver1000 = cities.stream()
      .noneMatch(city -&amp;gt; city.getArea() &amp;gt; 1000);

boolean allAreasAreNotOver2000 = cities.stream()
      .noneMatch(city -&amp;gt; city.getArea() &amp;gt; 2000);

System.out.println(allAreasAreNotOver1000);
System.out.println(allAreasAreNotOver2000);

// 결과
false
true&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;데이터 검색&lt;/h2&gt;
&lt;p&gt;검색 메소드를 사용하면 스트림 파이프라인에서 적절한 데이터를 찾는다.&lt;/p&gt;
&lt;p&gt;또한 데이터를 찾는 순간 검색이 종료되는 Short-circuit 기법이 적용된다.&lt;/p&gt;
&lt;p&gt;검색 조건에 부합하는 데이터가 없을 수도 있다. 따라서 반환 시엔 &lt;code&gt;Optional&lt;/code&gt; 을 사용한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::findAny&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;검색 조건에 부합하는 임의의 데이터를 반환한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;순서에 구애받지 않기 때문에 병렬 스트림을 생성하면 결과가 다르게 나올 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional&amp;lt;City&amp;gt; find = cities.stream()
      .filter(city -&amp;gt; city.getArea() &amp;gt; 500) // 검색 조건은 filter를 활용한다.
      .findAny();

System.out.println(find.get().getName());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Stream::findFirst&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;검색 조건에 부합하는 &lt;strong&gt;첫 번째&lt;/strong&gt; 데이터를 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional&amp;lt;City&amp;gt; find = cities.stream()
      .filter(city -&amp;gt; city.getArea() &amp;gt; 500)
      .findFirst();

System.out.println(find.get().getName());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;데이터 연산&lt;/h2&gt;
&lt;p&gt;지금까지는 스트림 원소에 독립적인 로직을 적용하였다.&lt;/p&gt;
&lt;p&gt;임의의 숫자가 담긴 배열의 총 합을 스트림을 활용하여 구해보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;데이터&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  Random random = new Random();

  int[] array = new int[100];
  for (int i = 0; i &amp;lt; 100; i++) {
      array[i] = random.nextInt(100);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;for loop 활용&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int sum = 0;
for (int num : array) {
    sum += num;
}
System.out.println(sum);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Stream 활용&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Stream::reduce&lt;/code&gt; 메소드는 값을 연쇄적으로 계산할 때 사용한다.&lt;/p&gt;
&lt;p&gt;reduce는 (int), &lt;code&gt;IntBinaryOperator&lt;/code&gt; 를 매개변수로 받는다.&lt;/p&gt;
&lt;p&gt;첫 번째 int는 초기값으로, 생략이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IntBinaryOperator&lt;/code&gt; 는 두 개의 int형 매개변수를 받아 int를 반환하는 함수형 인터페이스이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int sum = Arrays.stream(array)
        .reduce(0, Integer::sum);

System.out.println(sum);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;최종 연산&lt;/h2&gt;
&lt;p&gt;최종 연산은 자료형, 컬렉션, void를 반환한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Collection 반환&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;List&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;예제에서도 계속 사용했던 형태이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;lt;String&amp;gt; streamNameList = cities.stream()
      .filter(city -&amp;gt; city.getArea() &amp;gt; 800)
      .sorted(Comparator.comparing(City::getArea))
      .map(City::getName)
      .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;공통 요소를 그룹핑한다는 개념으로 &lt;code&gt;Map&lt;/code&gt;을 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Map&amp;lt;String, List&amp;lt;City&amp;gt;&amp;gt; cityMap = cities.stream()
      .filter(city -&amp;gt; city.getArea() &amp;gt; 800)
      .collect(Collectors.groupingBy(City::getName));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Set&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;원본 &lt;code&gt;List&lt;/code&gt; 에서 조건에 맞는 데이터만 선별하여 &lt;code&gt;Set&lt;/code&gt; 으로 다시 저장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Set&amp;lt;City&amp;gt; citySet = cities.stream()
      .filter(city -&amp;gt; city.getArea() &amp;gt; 800)
      .collect(Collectors.toSet());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;자료형 반환&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스트림의 원소 개수를 세는 예제&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  System.out.println(cities.stream().count());

  // 결과
  6&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;void&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;순회&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  cities.stream().map(City::getName).forEach(System.out::println);

  // 결과
  Seoul
  Incheon
  Ulsan
  Daegu
  Gwangju
  Busan&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Java</category>
      <category>Java</category>
      <category>Stream</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/103</guid>
      <comments>https://private-space.tistory.com/103#entry103comment</comments>
      <pubDate>Fri, 17 Jul 2020 15:12:45 +0900</pubDate>
    </item>
    <item>
      <title>Java Stream (3) 스트림의 개념</title>
      <link>https://private-space.tistory.com/102</link>
      <description>&lt;h1&gt;스트림&lt;/h1&gt;
&lt;p&gt;자바 언어 설계자들은 개발자의 시간을 절약해주면서도 쉽고 빠르게 코드를 작성할 수 있는 &lt;strong&gt;스트림&lt;/strong&gt;이라는 기능을 추가했다.&lt;/p&gt;
&lt;p&gt;아주 쉬운 예시를 통해 스트림의 특징을 파악해본다.&lt;/p&gt;
&lt;h3&gt;사용할 데이터&lt;/h3&gt;
&lt;p&gt;컬렉션과 스트림의 차이를 예시로 나타내기 위해 사용할 데이터를 만들었다.&lt;/p&gt;
&lt;p&gt;유형을 &lt;code&gt;City&lt;/code&gt; 클래스로 정의하고 대한민국의 광역시 목록을 &lt;strong&gt;cities&lt;/strong&gt;에 저장하였다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;City&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt; @Getter
 @AllArgsConstructor
 public class City {
     private String name;
     private double area;     // 면적
     private int population;  // 인구
     private String areaCode; // 지역 번호
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;cities&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt; List&amp;lt;City&amp;gt; cities = Arrays.asList(
         new City(&amp;quot;Seoul&amp;quot;, 605.2, 9720846, &amp;quot;02&amp;quot;),
         new City(&amp;quot;Incheon&amp;quot;, 1063.3, 2947217, &amp;quot;032&amp;quot;),
         new City(&amp;quot;Busan&amp;quot;, 770.1, 3404423, &amp;quot;051&amp;quot;),
         new City(&amp;quot;Gwangju&amp;quot;, 501.1, 1455048, &amp;quot;062&amp;quot;),
         new City(&amp;quot;Daegu&amp;quot;, 883.5, 2427954, &amp;quot;053&amp;quot;),
         new City(&amp;quot;Ulsan&amp;quot;, 1062, 1142190, &amp;quot;052&amp;quot;)
 );&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;cities 데이터를 가공해달라는 요구사항이 들어왔다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;면적(area)이 800 km^2 이상인 광역시의 목록을 추출한다.&lt;/li&gt;
&lt;li&gt;면적을 기준으로 오름차순 정렬한다.&lt;/li&gt;
&lt;li&gt;위의 기준을 충족한 광역시의 이름 목록만 반환받고자 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;컬렉션 처리&lt;/h3&gt;
&lt;p&gt;컬렉션으로 처리하면 보통 아래처럼 코드를 작성할 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;lt;City&amp;gt; largeCities = new ArrayList&amp;lt;&amp;gt;();

for (City city : cities) {
    if (city.getArea() &amp;gt; 800) {
        largeCities.add(city);
    }
}

Collections.sort(largeCities, new Comparator&amp;lt;&amp;gt;() {
    @Override
    public int compare(City o1, City o2) {
        return (int)(o1.getArea() - o2.getArea());
    }
});

List&amp;lt;String&amp;gt; largeCityNames = new ArrayList&amp;lt;&amp;gt;();

for (City city : largeCities) {
    largeCityNames.add(city.getName());
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;스트림 처리&lt;/h3&gt;
&lt;p&gt;같은 요구 사항을 스트림으로 처리한 코드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;lt;String&amp;gt; streamNameList = cities.stream()
        .filter(city -&amp;gt; city.getArea() &amp;gt; 800)
        .sorted(Comparator.comparing(City::getArea))
        .map(City::getName)
        .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;스트림 처리의 장점&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;선언형 처리&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터를 처리하는 로직을 직접 작성하지 않고 &lt;code&gt;.filter()&lt;/code&gt; 와 같이 연산을 정의한 메소드를 활용한다.&lt;/li&gt;
&lt;li&gt;처리 방법보다는 각 단계에서 수행할 로직의 목표(필터링 → 정렬 → 매핑 → 취합)를 명시한다.&lt;/li&gt;
&lt;li&gt;선언형 언어에 대한 &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%84%A0%EC%96%B8%ED%98%95_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot;&gt;참고자료 1&lt;/a&gt;, &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%84%A0%EC%96%B8%ED%98%95_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D_%EC%96%B8%EC%96%B4&quot;&gt;참고자료 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;간단한 병렬 처리&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;특정 환경에서 성능 향상을 기대할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cities.stream()&lt;/code&gt; ⇒ &lt;code&gt;cities.parallelStream()&lt;/code&gt; 으로 스트림 생성 부분을 교체한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  List&amp;lt;String&amp;gt; streamNameList = cities.parallelStream()
          .filter(city -&amp;gt; city.getArea() &amp;gt; 800)
          .sorted(Comparator.comparing(City::getArea))
          .map(City::getName)
          .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;필요한 부품을 하나씩 조립하듯 구성&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 연산을 체이닝으로 잇고 결과를 다음 로직으로 전달한다.&lt;/li&gt;
&lt;li&gt;필요한 연산만을 조립하여 유연하게 구성할 수 있다.&lt;/li&gt;
&lt;li&gt;모든 과정이 내부에서 반복된다.&lt;ul&gt;
&lt;li&gt;컬렉션은 필터링 시 외부 반복자를 사용하여 미해당 원소를 걸러낸다.&lt;/li&gt;
&lt;li&gt;스트림은 반복자가 겉으로 드러나지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;연산 과정에서 불필요한 변수를 남기지 않는다.&lt;ul&gt;
&lt;li&gt;컬렉션에선 정렬을 위한 &lt;code&gt;List&lt;/code&gt; 형 변수를 하나 사용했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위와 같은 특징은 컬렉션과 스트림의 &lt;strong&gt;지향점&lt;/strong&gt;이 다르기 때문에 나타나는 차이이다.&lt;/p&gt;
&lt;p&gt;컬렉션은 데이터를 저장하는 자료구조 구현에 초점이 맞추어져 있고, 스트림은 데이터를 연산하는 데에 집중한 API이기 때문이다.&lt;/p&gt;
&lt;h2&gt;스트림 연산&lt;/h2&gt;
&lt;p&gt;스트림은 연산이라고 불리우는 부품을 모아 하나의 흐름으로 완성시켜 사용한다.&lt;/p&gt;
&lt;p&gt;이를 스트림 파이프라인이라고 하며 그 흐름에는 3아래 가지가 반드시 포함되어야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;스트림 생성&lt;/li&gt;
&lt;li&gt;중간 연산&lt;/li&gt;
&lt;li&gt;최종 연산&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;스트림 생성&lt;/h3&gt;
&lt;p&gt;스트림을 생성하는 것으로 연산을 시작한다.&lt;/p&gt;
&lt;p&gt;생성된 스트림은 한 번 사용하면 사라지기 때문에 &lt;strong&gt;소비&lt;/strong&gt;한다는 개념으로 사용해야 한다.&lt;/p&gt;
&lt;h3&gt;중간 연산&lt;/h3&gt;
&lt;p&gt;중간 연산은 &lt;code&gt;Stream&lt;/code&gt; 을 반환한다.&lt;/p&gt;
&lt;p&gt;그 덕분에 중간 연산 메소드는 체이닝으로 다음 중간 연산 메소드를 이어줄 수 있다.&lt;/p&gt;
&lt;p&gt;체이닝하여 연결하는 각 연산은 스트림 파이프라인에 저장된다.&lt;/p&gt;
&lt;p&gt;가장 중요한 특징은 중간 연산을 아무리 많이 연결한들 파이프라인이 실질적으로 수행되기 전까진 아무 연산도 하지 않는다는 것이다.&lt;/p&gt;
&lt;p&gt;이를 &lt;strong&gt;Lazy Evaluation&lt;/strong&gt;라 한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Lazy Evaluation&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;스트림 연산은 최종 연산이 호출되기 전까지 미뤄진다.&lt;/p&gt;
&lt;p&gt;그 과정에서 연산에 불필요한 원소를 파악한 뒤 해당 원소를 사용한 연산은 회피하여 로직을 최적화한다.&lt;/p&gt;
&lt;h3&gt;최종 연산&lt;/h3&gt;
&lt;p&gt;중간 연산을 이어가며 만든 스트림 파이프라인에서 최종 결과를 도출하는 과정이다.&lt;/p&gt;
&lt;p&gt;최종 연산이 끝나면 컬렉션(&lt;code&gt;List&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;)이나 자료형(&lt;code&gt;Integer&lt;/code&gt;), void를 반환한다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>Java</category>
      <category>Stream</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/102</guid>
      <comments>https://private-space.tistory.com/102#entry102comment</comments>
      <pubDate>Wed, 15 Jul 2020 01:42:09 +0900</pubDate>
    </item>
    <item>
      <title>Java Stream (2) 람다 표현식</title>
      <link>https://private-space.tistory.com/101</link>
      <description>&lt;h1&gt;람다 표현식&lt;/h1&gt;
&lt;p&gt;자바에서는 인터페이스를 선언하거나 매개변수로 주어야 할 때 1회용 구현체인 &lt;b&gt;익명 클래스&lt;/b&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;아래 예시에서 &lt;code&gt;Test::printList&lt;/code&gt; 의 변화를 확인해본다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class Test {

    public static void main(String[] args) {

        List&amp;lt;Integer&amp;gt; list = Arrays.asList(2, 1, 3, 5, 4);
        printList(list);

        // 오름차순 정렬
        List&amp;lt;Integer&amp;gt; list2 = list.stream()
                .sorted(Comparator.naturalOrder())
                .collect(Collectors.toList());
        printList(list2);

        // 내림차순 정렬
        List&amp;lt;Integer&amp;gt; list3 = list.stream()
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
        printList(list3);
    }

    private static void printList(List&amp;lt;Integer&amp;gt; list) {

        // 1회용 구현체 new Consumer를 매개변수로 활용
        list.forEach(new Consumer&amp;lt;Integer&amp;gt;() {
            @Override
            public void accept(Integer num) {
                System.out.println(num + &quot; &quot;);
            }
        });
        System.out.println();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private static void printList(List&amp;lt;Integer&amp;gt; list) {
    list.forEach(num -&amp;gt; System.out.println(num + &quot; &quot;));
    System.out.println();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;람다를 활용하여 &lt;code&gt;Test::printList&lt;/code&gt; 의 내용을 더 간결하게 변경했다.&lt;/p&gt;
&lt;p&gt;익명 클래스 &lt;code&gt;new Consumer&amp;lt;Interger&amp;gt;() {...}&lt;/code&gt; 가 단 한 줄로 줄어들었으며 개발자의 의도가 드러나는 문장을 사용했다.&lt;/p&gt;
&lt;p&gt;forEach의 매개변수에 사용되는 &lt;code&gt;Consumer&lt;/code&gt; 타입과 &lt;code&gt;Consumer::accept&lt;/code&gt; 가 겉으로 드러나지 않는다.&lt;/p&gt;
&lt;p&gt;람다식은 과도한 정보를 제하고 num을 출력하는 로직에 더 집중할 수 있는 구조인 것이다.&lt;/p&gt;
&lt;h2&gt;람다 표현식?&lt;/h2&gt;
&lt;p&gt;람다식은 익명 클래스를 간략하게 표현하는 방법이다.&lt;/p&gt;
&lt;p&gt;식 전체가 인스턴스로 취급된다.&lt;/p&gt;
&lt;h2&gt;람다 표현식의 구조&lt;/h2&gt;
&lt;p&gt;람다 표현식은 기본적으로 아래와 같은 구조를 가진다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;(parameters) -&amp;gt; expression
(parameters) -&amp;gt; { statements; }&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;람다 표현식엔 인터페이스와 오버라이드할 메소드의 이름이 표기되지 않는다.&lt;/li&gt;
&lt;li&gt;매개변수 목록을 왼쪽에 표기한다.
&lt;ul&gt;
&lt;li&gt;매개변수는 타입을 생략할 수 있다.&lt;/li&gt;
&lt;li&gt;매개변수가 1개이면 괄호를 생략할 수 있다.&lt;/li&gt;
&lt;li&gt;매개변수가 0개이면 &lt;code&gt;() -&amp;gt;&lt;/code&gt;으로 매개변수가 없음을 나타내어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매개변수 목록 작성이 끝나면 화살표 (&lt;code&gt;-&amp;gt;&lt;/code&gt;)를 작성한다.&lt;/li&gt;
&lt;li&gt;화살표 우측에는 람다 바디를 작성한다.
&lt;ul&gt;
&lt;li&gt;한 줄(세미콜론 한 번)으로 구현할 수 있는 경우 expression이다.&lt;/li&gt;
&lt;li&gt;expression은 중괄호, return, 세미콜론을 생략하여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;1. expression 구현
(String s) -&amp;gt; {return s.length();} &amp;gt;&amp;gt;&amp;gt; s -&amp;gt; s.length() 으로 변경 가능
(Human h) -&amp;gt; h.getAge() &amp;gt; 10       &amp;gt;&amp;gt;&amp;gt; 사람의 나이가 11살 이상임을 검사

2. 매개변수가 없는 경우
() -&amp;gt; System.out.println(&quot;Hello, world!&quot;)

3. statement 구현
(int x, int y) -&amp;gt; {
    int z = x + y;
    System.out.println(z);
    return z;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;람다 표현식 사용 범위&lt;/h2&gt;
&lt;p&gt;모든 인터페이스를 람다식으로 표현할 수 있는 건 아니다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;함수형 인터페이스&lt;/b&gt;에만 사용이 가능하다.&lt;/p&gt;
&lt;h3&gt;함수형 인터페이스&lt;/h3&gt;
&lt;p&gt;&lt;b&gt;함수형 인터페이스&lt;/b&gt;는 추상 메소드가 &lt;b&gt;단 1개&lt;/b&gt;만 있는 인터페이스를 의미한다.&lt;/p&gt;
&lt;p&gt;특정 부모에게 상속받아 오버라이드한 클래스가 있는 경우는 함수형 인터페이스가 아니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@FunctionalInterface&lt;/code&gt; 으로 이 인터페이스가 함수형 인터페이스임을 표시할 수 있다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;@FunctionalInterface&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;함수형 인터페이스를 표현하는 어노테이션이다.&lt;/p&gt;
&lt;p&gt;이 어노테이션을 사용했으나 추상 메소드가 2개 이상이면 함수형 인터페이스가 아니라고 판단하여 경고를 내뱉는다.&lt;/p&gt;
&lt;h2&gt;자주 사용하는 함수형 인터페이스&lt;/h2&gt;
&lt;p&gt;자바 API는 외우기보다 활용 방법을 이해하고 쓰는 것이 일반적이다.&lt;/p&gt;
&lt;p&gt;다만 자바 8에서 새로 추가된 함수형 인터페이스는 어느 정도 암기하는 편이 생산성에 좋을 것 같다.&lt;/p&gt;
&lt;p&gt;람다식엔 클래스 타입을 쓰지 않으니 자바가 이미 지원하는 함수형 인터페이스를 중복으로 만들지 않도록 해야 한다.&lt;/p&gt;
&lt;h3&gt;조건 검증&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Predicate&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;T 타입을 매개변수로 받고 boolean을 반환한다.&lt;/li&gt;
&lt;li&gt;조건 검증에 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BiPredicate&amp;lt;L, R&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;L, R을 받고 boolean을 반환한다.&lt;/li&gt;
&lt;li&gt;주어진 두 매개변수를 사용하여 검증할 때 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;자원 생성/소모&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Consumer&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;T 타입을 매개변수로 받고 반환 타입이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Supplier&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;매개변수는 없으며 T 타입을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BiConsumer&amp;lt;T, U&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;T, U를 받고 반환은 하지 않는다.&lt;/li&gt;
&lt;li&gt;매개변수가 2개 필요하지만 반환 없이 자원을 소모하는 연산에 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;연산&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Function&amp;lt;T, R&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;T 타입을 매개변수로 받고 R 타입을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UnaryOperator&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Function&amp;lt;T, R&amp;gt;&lt;/code&gt; 을 상속받은 인터페이스이며 T, R을 같은 타입으로 사용한다.&lt;/li&gt;
&lt;li&gt;T를 받고 T를 반환한다.&lt;/li&gt;
&lt;li&gt;단항 연산 시 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BiFunction&amp;lt;T, U, R&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;T, U를 매개변수로 받고 R을 반환한다.&lt;/li&gt;
&lt;li&gt;T, U, R 모두 다른 타입을 유연하게 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BinaryOperator&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BiFunction&amp;lt;T, U, R&amp;gt;&lt;/code&gt; 을 상속받은 인터페이스이며 T, U, R을 같은 타입으로 사용한다.&lt;/li&gt;
&lt;li&gt;T, T를 받고 T를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;매개변수와 반환형이 중복되는 함수형 인터페이스&lt;/h3&gt;
&lt;p&gt;람다식은 인터페이스 이름과 오버라이드할 메소드 이름을 명시적으로 나타내지 않는다.&lt;/p&gt;
&lt;p&gt;매개변수와 반환 타입이 완전 같은 함수형 인터페이스가 2개 이상 존재하는 경우 누구를 가리키는지 명확하지 않다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Foo1&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt; @FunctionalInterface
 public interface Foo1 {
     void test();
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Foo2&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt; @FunctionalInterface
 public interface Foo2 {
     void test();
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;foo&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt; void foo(Foo1 foo1) { }

 foo((Foo1) () -&amp;gt; {});&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;foo에서 매개변수를 &lt;code&gt;Foo1&lt;/code&gt; 으로 받고 있으며 컴파일러도 당연히 &lt;code&gt;Foo1&lt;/code&gt; 을 기대할 것이다.&lt;/p&gt;
&lt;p&gt;그러나 개발자 입장에선 명확하게 와닿지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Foo1&lt;/code&gt; 을 캐스팅하면 어느 인터페이스를 활용하는지 확실하게 표기할 수 있다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>Java</category>
      <category>Stream</category>
      <category>람다</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/101</guid>
      <comments>https://private-space.tistory.com/101#entry101comment</comments>
      <pubDate>Wed, 8 Jul 2020 18:11:42 +0900</pubDate>
    </item>
    <item>
      <title>Java Stream (1) 스트림 개요</title>
      <link>https://private-space.tistory.com/99</link>
      <description>&lt;h1&gt;Java Stream 개요&lt;/h1&gt;
&lt;p&gt;자바는 90년대 공개된 이후 현재도 가장 인기가 많은 언어 중 하나로 손꼽히고 있다.&lt;/p&gt;
&lt;p&gt;이유는 간단하다. 사용자가 필요로 하는 기능이 자바에 계속 추가되니까.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;2014년 공개된 자바 8엔 함수형 프로그래밍을 지원하는 스트림이 포함되었다.&lt;/p&gt;
&lt;p&gt;스트림의 추가는 자바를 완전히 새로운 형태로 작성할 수 있는 새로운 장을 열었다고 보아도 무방하다.&lt;/p&gt;
&lt;p&gt;'함축적이고 짧아진 코드' 그 이상을 제공하고 있기 때문이다.&lt;/p&gt;
&lt;h2&gt;자바 8 설계에 쓰인 프로그래밍 개념&lt;/h2&gt;
&lt;p&gt;자바 8을 설계하는데 고려한 프로그래밍 세 가지 개념을 소개한다.&lt;/p&gt;
&lt;h3&gt;  스트림 처리&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;스트림은 한 번에 한 개씩 만들어지는 연속적인 데이터 항목의 모임이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이론적으로 프로그램은 입력 스트림에서 하나씩 읽어들이고, 출력 스트림에 하나씩 기록한다.&lt;/p&gt;
&lt;p&gt;한 프로그램의 출력 스트림이 다른 프로그램의 입력 스트림이 될 수도 있다.&lt;/p&gt;
&lt;p&gt;이미 리눅스 커맨드 쉘에서 많이 활용하고 있는 기능일 것이다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;ls -l . | grep txt | sort | tail -3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 명령어는 현재 디렉토리에서 &lt;b&gt;txt&lt;/b&gt;를 포함하는 모든 파일을 찾아낸 뒤 정렬하고, 그 결과의 맨 아래 3줄만 출력한다.&lt;/p&gt;
&lt;p&gt;순차적으로 넘기는 것처럼 보이지만 각 명령어는 병렬로 처리될 수 있다.&lt;/p&gt;
&lt;p&gt;grep이 완료되기 전에 sort가 동작하는 것이다.&lt;/p&gt;
&lt;p&gt;자바 8의 스트림도 병렬 처리가 가능한데, 스레드와 같은 복잡한 처리가 필요하지 않아 매우 간단하다.&lt;/p&gt;
&lt;h3&gt;  행위 매개변수화 (Behavior parameterization)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;코드를 API로 전달한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;더 쉽게 말하면 메소드를 매개변수로 사용할 수 있다는 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;sort의 정렬 기준?&lt;/p&gt;
&lt;p&gt;위 리눅스 명령어 예제에서 sort의 기준이 무엇일까? 가끔 바꾸고 싶을 때가 있지 않을까?&lt;/p&gt;
&lt;p&gt;기본이 오름차순이라면 내림차순 기준으로 변경하고 싶을 수도 있다.&lt;/p&gt;
&lt;p&gt;자바로 구현한다고 생각한다면, &lt;code&gt;new Comparator() { ...}&lt;/code&gt; 를 직접 만들어서 기준을 바꿔주면 된다.&lt;/p&gt;
&lt;p&gt;자바 7까지는 위와 같은 흐름으로 개발했다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;스트림을 활용하면?&lt;/p&gt;
&lt;p&gt;스트림은 기존 로직을 크게 해치지 않으면서 내 코드를 주입하듯이 구현할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;331&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JZDsO/btqEw2mmT0j/dxkpXoUsBp0mkKN1Pdh5t0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JZDsO/btqEw2mmT0j/dxkpXoUsBp0mkKN1Pdh5t0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JZDsO/btqEw2mmT0j/dxkpXoUsBp0mkKN1Pdh5t0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJZDsO%2FbtqEw2mmT0j%2FdxkpXoUsBp0mkKN1Pdh5t0%2Fimg.png&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;331&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;⛓ 병렬성과 공유 데이터&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;보다 쉽게 처리하는 병렬성&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;원활한 병렬 처리를 위해서는 공유 데이터를 사용하지 않는 것이 일반적이다.&lt;/p&gt;
&lt;p&gt;동시 접근 시 의도와는 다른 결과가 나타날 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;자바에서는 공유 데이터에 대한 접근 제어를 &lt;code&gt;synchronized&lt;/code&gt; 와 같은 키워드를 활용해서 처리했으나 속도가 굉장히 느리다는 단점이 있다.&lt;/p&gt;
&lt;p&gt;자바 8 스트림을 활용하면 병렬 처리를 더 쉽게 할 수 있으면서도 속도를 크게 해치지 않는다.&lt;/p&gt;
&lt;h2&gt;자바와 함수&lt;/h2&gt;
&lt;p&gt;자바는 함수보다는 메소드라는 용어를 사용한다. (&lt;a href=&quot;https://stackoverflow.com/questions/22913321/why-functions-are-called-methods-in-java&quot;&gt;Why functions are called methods in java?&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;그러나 자바 8 스트림에서는 &lt;b&gt;함수&lt;/b&gt;라는 용어를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;객체의 메소드보다 더 일반적인 형태로 사용하고자 정의된 요소가 필요하기 때문이다.&lt;/p&gt;
&lt;h3&gt;1급 시민&lt;/h3&gt;
&lt;p&gt;자바스크립트에서는 함수 자체가 값을 가지게 할 수 있다.&lt;/p&gt;
&lt;p&gt;이런 함수를 1급 시민이라고 한다.&lt;/p&gt;
&lt;p&gt;자바의 클래스는 그 자체로는 값을 가지지 않기 때문에 2급 시민이다.&lt;/p&gt;
&lt;h3&gt;메소드 참조와 1급 시민&lt;/h3&gt;
&lt;p&gt;자바 8에서 새롭게 추가된 특징인 메소드 참조 기능을 활용하면 메소드를 1급 시민으로 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;// OLD
File[] hiddenFiles = new File(&quot;.&quot;).listFiles(new FileFileter() {
    public boolean accept(File file) {
        return file.isHidden();
    }
});

// NEW
File[] hiddenFiles = new File(&quot;.&quot;).listFiles(File::isHidden);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;File&lt;/code&gt; 의 isHidden 메소드 자체를 값으로 전달했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;File::isHidden&lt;/code&gt; 대신 &lt;code&gt;(File file) -&amp;gt; file.isHidden()&lt;/code&gt; 을 사용하면 익명 람다 함수로 전달한다.&lt;/p&gt;
&lt;h2&gt;&lt;span&gt; &lt;/span&gt;스트림&lt;/h2&gt;
&lt;p&gt;자바의 Collection은 다양한 자료 구조가 구현되어 있다.&lt;/p&gt;
&lt;p&gt;개발자는 필요한 것을 가져다 쓰기만 하면 된다.&lt;/p&gt;
&lt;p&gt;그러나 코드가 복잡해진다는 단점이 존재했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// OLD
Map&amp;lt;String, SomeObject&amp;gt; map = new HashMap&amp;lt;&amp;gt;();

for (SomeObject obj : objs) {
    if (obj.isObject()) {
        map.put(obj.getName(), obj);
    }
}

// NEW
Map&amp;lt;String, SomeObject&amp;gt; map = 
    objs.stream()
        .filter((SomeObject obj) -&amp;gt; obj.isObject())
        .collect(groupingBy(SomeObject::getName));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예시가 워낙 짧아 컬렉션을 활용한 코드가 더 단순해 보인다. (본인의 상상력을 활용하여 긴 코드라고 생각해보자...)&lt;/p&gt;
&lt;p&gt;전자는 논리적인 흐름으로 파악할 수 있다면 후자는 적절한 이름으로 이어가며 처리한다.&lt;/p&gt;
&lt;p&gt;또 중요한 차이가 있는데, foreach에서는 각 요소를 반복하며 데이터를 처리하지만 스트림 API는 라이브러리 내부에서 모든 데이터를 처리한다.&lt;/p&gt;
&lt;p&gt;이를 &lt;b&gt;내부 반복(Internal iteration)&lt;/b&gt; 이라고 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;objs에 너무 많은 데이터가 담겨있다면?&lt;/p&gt;
&lt;p&gt;foreach문에서는 데이터를 처리하는데 많은 시간을 소요한다.&lt;/p&gt;
&lt;p&gt;그러나, stream은 병렬 처리를 쉽게 구현할 수 있기 때문에 더 빠르게 처리할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>Java</category>
      <category>Stream</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/99</guid>
      <comments>https://private-space.tistory.com/99#entry99comment</comments>
      <pubDate>Tue, 2 Jun 2020 01:39:55 +0900</pubDate>
    </item>
    <item>
      <title>Spring에서 AOP를 구현하는 방법과 Transactional</title>
      <link>https://private-space.tistory.com/98</link>
      <description>&lt;h1&gt;Spring에서 AOP를 구현하는 방법과 Transactional&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP에 관한 간략한 개념이 필요하다면 &lt;a href=&quot;https://private-space.tistory.com/62&quot;&gt;다른 글&lt;/a&gt;을 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용은 공식 문서를 참조하여 작성하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-introduction-proxies&quot;&gt;Spring Framework Document&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AOP 구현 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 AOP는 두 가지 방법으로 구현되어 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dynamic Proxy (Reflection)&lt;/li&gt;
&lt;li&gt;CGLIB (Byte Code Instrument)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;591&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJLcS5/btqEnIg9ftX/anuzbYTkMtcnV7dk6hX4x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJLcS5/btqEnIg9ftX/anuzbYTkMtcnV7dk6hX4x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJLcS5/btqEnIg9ftX/anuzbYTkMtcnV7dk6hX4x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJLcS5%2FbtqEnIg9ftX%2FanuzbYTkMtcnV7dk6hX4x1%2Fimg.png&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;591&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스를 클래스로 구현하여 사용하는 경우 Dynamic proxy, 단일 클래스는 CGLIB를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 인터페이스에 선언되지 않은 메소드를 권고하는 경우
2. 메소드의 인자로 프록시된 객체를 구체적으로 전달해야 하는 경우&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 비즈니스 로직을 구현할 때 클래스보다는 인터페이스로 유연하게 설계할 것을 권장하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 인터페이스와 구현체로 이루어진 구성에서는 어지간하면 Dynamic proxy가 사용되는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dynamic Proxy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynamic Proxy 활용 시 사용하는 &lt;code&gt;JdkDynamicAopProxy&lt;/code&gt; 클래스의 주석엔 이 방식의 특징이 적혀있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;java.lang.reflect.Proxy&lt;/code&gt; 를 기반으로 하는 AOP&lt;/li&gt;
&lt;li&gt;인터페이스가 구현된 클래스에만 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;대상 클래스가 thread-safe한 경우 생성된 프록시도 thread-safe하다.&lt;/li&gt;
&lt;li&gt;Advice/Pointcut 및 TargetSource가 직렬화 가능하다면 프록시도 직렬화가 가능하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CGLIB&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGLIB는 byte code를 조작하여 코드를 생성하는 기법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 CGLIB 기반 프록시를 수행하는 클래스는 &lt;code&gt;CglibAopProxy&lt;/code&gt; 이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CGLIB도 Dynamic Proxy와 마찬가지로 원본 클래스의 thread-safe 특징을 따른다.&lt;/li&gt;
&lt;li&gt;부모 인터페이스가 없는 클래스에도 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;상속을 활용한다. (상속이 불가능한 final/private 키워드는 aspect가 적용되지 않는다)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Transactional&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP 기능 중 가장 많이 활용되는 &lt;code&gt;@Transactional&lt;/code&gt; 은 어떻게 동작할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring boot에 약간의 코드를 작성하고 디버깅으로 추적해보았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SpringAopTest&lt;/code&gt;&lt;/li&gt;
&lt;li class=&quot;routeros&quot;&gt;&lt;code&gt; public interface SpringAopTest {
     void test1();
 }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SpringAopTestImpl&lt;/code&gt;&lt;/li&gt;
&lt;li class=&quot;less&quot;&gt;&lt;code&gt; @Component
 public class SpringAopTestImpl implements SpringAopTest{

     @Transactional
     @Override
     public void test1() {
         System.out.println(&quot;TEST1 - Begin&quot;);
         Assert.isTrue(true == false, &quot;test1&quot;);
         System.out.println(&quot;TEST1 - End&quot;);
     }
 }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AppRunner&lt;/code&gt;&lt;/li&gt;
&lt;li class=&quot;java&quot;&gt;&lt;code&gt; @Component
 public class AppRunner implements ApplicationRunner {

     private SpringAopTest springAopTest;

     public AppRunner(SpringAopTest springAopTest) {
         this.springAopTest = springAopTest;
     }

     @Override
     public void run(ApplicationArguments args) throws Exception {
         springAopTest.test1();
     }
 }&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Assert 조건에 의해 무조건 실패하게 되는 코드를 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AppRunner#run&lt;/code&gt; , &lt;code&gt;SpringAopTestImpl#test1&lt;/code&gt; 의 첫 줄에 중단점을 걸어두고 디버깅을 진행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &lt;code&gt;CglibAopProxy&lt;/code&gt; 로 넘어가는 것으로 보아 &lt;code&gt;@Transactional&lt;/code&gt; 은 CGLIB를 활용함을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서엔 인터페이스를 구현한 경우 Dynamic Proxy를 활용한다고 했는데 실제로는 CGLIB를 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 대체 어디서 CGLIB를 기본으로 설정했는지 로그로 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 레벨을 TRACE로 바꾸고 코드를 실행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.yml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1621923070520&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;logging:
  level:
    org.springframework: TRACE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면 무수히 많은 로그가 쏟아지는데, cglib로 검색하다보면 눈에 띄는 로그가 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Condition OnPropertyCondition on org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration$CglibAutoProxyConfiguration matched due to @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TransactionAutoConfiguration&lt;/code&gt; 를 따라가면 내부 클래스로 &lt;code&gt;EnableTransactionManagementConfiguration&lt;/code&gt; 가 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {

    @Configuration(proxyBeanMethods = false)
    @EnableTransactionManagement(proxyTargetClass = false)
    @ConditionalOnProperty(prefix = &quot;spring.aop&quot;, name = &quot;proxy-target-class&quot;, havingValue = &quot;false&quot;,
            matchIfMissing = false)
    public static class JdkDynamicAutoProxyConfiguration {

    }

    @Configuration(proxyBeanMethods = false)
    @EnableTransactionManagement(proxyTargetClass = true)
    @ConditionalOnProperty(prefix = &quot;spring.aop&quot;, name = &quot;proxy-target-class&quot;, havingValue = &quot;true&quot;,
            matchIfMissing = true)
    public static class CglibAutoProxyConfiguration {

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드를 보면 AOP의 방식을 결정하는 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로 CGLIB를 따르고 있고 (&lt;b&gt;matchIfMissing = true&lt;/b&gt;), config 값에 따라 방식을 바꿀 수 있는 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정파일을 조금 바꾸어보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.yml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1621923155384&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  aop:
    proxy-target-class: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 후 디버깅하면 &lt;code&gt;JdkDynamicAopProxy&lt;/code&gt; 클래스로 넘어가는 것을 확인할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 따라가다 프록시 변경 방법을 발견했지만 &lt;a href=&quot;https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-pfb-proxy-types&quot;&gt;스프링 공식 문서&lt;/a&gt;을 다시 읽다 보니 6.4.3 항에 해당 내용을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 검색을 하다 &lt;b&gt;Spring boot&lt;/b&gt;를 활용하는 경우는 CGLIB를 기본으로 적용했다는 내용을 발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGLIB가 예외를 덜 발생시킨다는 장점이 있어 변경했다고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 DB Transaction은 일관된 비즈니스 로직을 만들기 위한 기능으로, 실패한 경우 작업한 데이터를 모두 롤백해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Transactional&lt;/code&gt; 역시도 롤백처리를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Assert에서 무조건 터지는 조건을 넣어주었기 때문에 이 부분부터 디버깅을 시작하고 진행하다 보면&lt;code&gt;TransactionAspectSupport&lt;/code&gt; 클래스로 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TransactionAspectSupport#invokeWithinTransaction&lt;/code&gt; 은 트랜잭션 기능 활용 시 사용되는 메소드로, 중간에 try/catch 문이 하나 보인다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;try {
    // This is an around advice: Invoke the next interceptor in the chain.
    // This will normally result in a target object being invoked.
    retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
    // target invocation exception
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령을 대신 수행하고 예외상황 발생 시 completeTransactionAfterThrowing 메소드에서 에러 처리를 하도록 넘겨준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라가면 롤백처리를 담당하는 아래의 코드가 보인다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 &lt;code&gt;@Transactional&lt;/code&gt; 을 수행하는 메소드는 반드시 Assert 등으로 잘못된 데이터에 대한 예외를 던지도록 처리해야 한다.&lt;/p&gt;</description>
      <category>Java/Spring framework</category>
      <category>Spring</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/98</guid>
      <comments>https://private-space.tistory.com/98#entry98comment</comments>
      <pubDate>Tue, 26 May 2020 02:15:47 +0900</pubDate>
    </item>
    <item>
      <title>트랜잭션 격리 수준(Isolation Level)</title>
      <link>https://private-space.tistory.com/97</link>
      <description>&lt;h1&gt;트랜잭션 격리 수준&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다 보면 여러 스레드에서 동시에 하나의 자원에 접근하는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근을 적절하게 제한하지 않는다면 생각지 못한 버그가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 이런 경우는 디버깅하기도 굉장히 어렵다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스도 N개의 트랜잭션을 동시에 처리하다 보면 같은 데이터에 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 목적은 로직의 흐름 속에서 데이터를 일관되게 처리하기 위한 것이나, 동시에 같은 데이터에 write 접근을 하는 경우엔 문제가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 문제를 해결하기 위해 데이터베이스는 격리 수준이라는 기능을 제공한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;격리 수준의 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리 수준은 4개로 구분된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;READ UNCOMMITTED&lt;/li&gt;
&lt;li&gt;READ COMMITTED&lt;/li&gt;
&lt;li&gt;REPEATABLE READ&lt;/li&gt;
&lt;li&gt;SERIALIZABLE&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4에 가까울 수록 동시성 접근 제어의 수준이 강해지며 동시 처리 성능은 낮아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근 제어를 강하게 수행한다는 것은 트랜잭션이 끝나기 전까지 해당 데이터를 잠그고 다른 트랜잭션에서 참조하지 못하게 하는 것이기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;READ UNCOMMITTED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋되지 않은 데이터에 접근이 가능한 수준으로, N개의 트랜잭션이 하나의 공유 데이터에 접근해도 전혀 보호되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;589&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u1BKr/btqEcois9nW/QgZsUlGnEHWoNHakmJ5g21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u1BKr/btqEcois9nW/QgZsUlGnEHWoNHakmJ5g21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u1BKr/btqEcois9nW/QgZsUlGnEHWoNHakmJ5g21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu1BKr%2FbtqEcois9nW%2FQgZsUlGnEHWoNHakmJ5g21%2Fimg.png&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;589&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 트랜잭션 1을 시작한다.
2. 트랜잭션 2를 시작한다.
3. 트랜잭션 1이 ID = 1, VAL = 'MIN'인 데이터의 VAL을 KIM으로 변경했다.
4. 트랜잭션 2가 ID = 1을 조회한다. VAL = 'KIM'이 조회되었다.
5. 트랜잭션 1, 2가 종료된다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 수준에서만 볼 수 있는 문제는 &lt;code&gt;Dirty Read&lt;/code&gt; 가 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dirty Read?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UPDATE 반영 전에 읽는 오류
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순서 5에서 오류가 생겨 롤백이 되었다고 가정하자. &lt;code&gt;ID = 1&lt;/code&gt;은 다시 MIN 이 된다.&lt;/li&gt;
&lt;li&gt;그러나 롤백 전인 순서 4번은 여전히 &lt;code&gt;VAL = 'KIM'&lt;/code&gt; 으로 인식할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;INSERT 반영 전에 읽는 오류
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 1이 특정 데이터를 INSERT한다.&lt;/li&gt;
&lt;li&gt;트랜잭션 2가 그 데이터를 읽고 로직을 수행한다.&lt;/li&gt;
&lt;li&gt;트랜잭션 1 수행 중 오류가 생겨 롤백된다. INSERT한 데이터가 삭제되었다.&lt;/li&gt;
&lt;li&gt;그러나 트랜잭션 2는 이미 로직을 수행한 상태이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;READ COMMITTED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋된 데이터만 조회할 수 있어 &lt;code&gt;Dirty read&lt;/code&gt;는 발생하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Untitled 1.png&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;787&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oXqMg/btqEeqFWXsr/d5WDuP5MjIawLjNQen8Nb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oXqMg/btqEeqFWXsr/d5WDuP5MjIawLjNQen8Nb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oXqMg/btqEeqFWXsr/d5WDuP5MjIawLjNQen8Nb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoXqMg%2FbtqEeqFWXsr%2Fd5WDuP5MjIawLjNQen8Nb1%2Fimg.png&quot; data-filename=&quot;Untitled 1.png&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;787&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 트랜잭션 1을 시작한다.
2. 트랜잭션 1이 ID = 1인 데이터의 VALUE를 KIM으로 변경했다.
3. 트랜잭션 2가 시작되었다.
4. 트랜잭션 2가 ID = 1인 데이터를 조회한다. MIN이 검색된다.
5. 트랜잭션 1이 커밋을 하고 종료한다.
6. 트랜잭션 2가 ID = 1인 데이터를 조회한다. KIM이 검색된다.
7. 트랜잭션 2가 커밋을 하고 종료한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋된 데이터만을 읽어오기 때문에 &lt;code&gt;READ UNCOMMITED&lt;/code&gt; 에서 롤백되는 경우 발생하는 문제는 생기지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 격리 수준 이하에선 &lt;code&gt;Non-Repeatable Read&lt;/code&gt; 문제가 발생한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-Repeatable Read?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 트랜잭션이 같은 값을 조회할 때 다른 값이 검색되는 현상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림에서 트랜잭션 2의 첫 번째 조회엔 MIN이, 두 번째 조회엔 KIM이 검색되고 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REPEATABLE READ&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 시작되고 종료되기 전까지 한 번 조회한 값은 계속 같은 값이 조회되는 격리 수준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 시작 전에 커밋된 내용에 한해서만 조회된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 변경하려고 하면 &lt;code&gt;UNDO&lt;/code&gt; 영역에 백업해두고 실제 레코드를 변경하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 격리 수준에서는 &lt;code&gt;Non-Repeatable Read&lt;/code&gt; 는 발생하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;874&quot; data-filename=&quot;1.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mMt8U/btq5JCNMucD/QwCb2y8vL4RgNQNeXyX6s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mMt8U/btq5JCNMucD/QwCb2y8vL4RgNQNeXyX6s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mMt8U/btq5JCNMucD/QwCb2y8vL4RgNQNeXyX6s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmMt8U%2Fbtq5JCNMucD%2FQwCb2y8vL4RgNQNeXyX6s0%2Fimg.png&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;874&quot; data-filename=&quot;1.png&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;1. 트랜잭션 1을 시작한다.
2. 트랜잭션 1이 ID = 1인 데이터를 조회한다.
3. 트랜잭션 2가 시작되었다.
4. 트랜잭션 2가 ID = 1인 데이터를 KIM으로 변경한다.
5. 트랜잭션 1이 ID = 1인 데이터를 조회한다. 트랜잭션 2의 변경 내역이 보이지 않는다.
6. 트랜잭션 2가 ID = 2인 데이터를 삽입 후 commit하여 트랜잭션을 종료한다.
7. 트랜잭션 1이 ID = 2인 데이터를 조회한다. 데이터가 정상적으로 확인된다.
8. 트랜잭션 1이 종료된다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 격리 수준에서는 &lt;b&gt;UPDATE&lt;/b&gt; 한 데이터에 대해서는 정합성을 보장하지만, &lt;b&gt;INSERT/DELETE&lt;/b&gt; 는 보장되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 이 격리 수준 이하에서는 &lt;code&gt;Phantom Read&lt;/code&gt; 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Phantom Read?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 유령을 보는 것처럼 있던 데이터가 사라지거나 없던 데이터가 생기는 현상을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 위 예제의 순서 7에서 트랜잭션 1이 ID = 2인 데이터를 조회하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID = 2는 트랜잭션 1이 시작하던 시점에선 테이블에 없던 데이터이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 순서 8에서 트랜잭션 2가 비정상 종료되어 롤백되었다고 가정한다면 (2, 'KIM') 데이터는 삽입되지 않으며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 1은 ID = 2인 데이터를 읽을 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SERIALIZABLE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 특정 테이블을 읽으면 다른 트랜잭션은 그 테이블의 데이터를 추가/변경/삭제할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 강력한 격리 수준이며 데이터 정합성을 가장 잘 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 동시 처리 성능이 가장 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 격리 수준에서는 위에서 언급했던 &lt;code&gt;Dirty Read&lt;/code&gt;, &lt;code&gt;Non-Repeatable Read&lt;/code&gt;, &lt;code&gt;Phantom Read&lt;/code&gt; 와 같은 정합성 문제가 전혀 발생하지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 정합성과 동시 처리 성능은 반비례하기 때문에 어떤 격리 수준이 무조건 좋다 나쁘다를 말하기는 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 어떻게 다룰지에 따라 적절한 전략을 선택하는 것이 중요하다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>Database</category>
      <category>Isolation</category>
      <category>transaction</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/97</guid>
      <comments>https://private-space.tistory.com/97#entry97comment</comments>
      <pubDate>Sun, 17 May 2020 14:12:33 +0900</pubDate>
    </item>
    <item>
      <title>Spring(boot)에서 초기화 코드를 작성하는 방법</title>
      <link>https://private-space.tistory.com/96</link>
      <description>&lt;h1&gt;Spring boot 초기화 코드 작성하기&lt;/h1&gt;
&lt;p&gt;서버가 켜지자마자 상태를 지정해줘야 하는 일들이 더러 있을 것이다.&lt;/p&gt;
&lt;p&gt;몇 가지만 알아두면 요긴하게 사용할 수 있다.&lt;/p&gt;
&lt;h2&gt;EventListener와 Runner를 사용한 방법&lt;/h2&gt;
&lt;p&gt;Spring boot에서만 사용할 수 있다.&lt;br /&gt;관련 이벤트와 Runner는 boot 패키지에 포함되어 있기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public ConfigurableApplicationContext run(String... args) {

    // Do something...
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        // Do something...

        // spring context
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);

        // Do something...
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
    // Do something...
    }

    // Do something...

    return context;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음으로 나타나는 try 문에서 context 관련 라이프사이클이 한 번 수행된 후 listeners.started(), callRunners()를 실행한다.&lt;/p&gt;
&lt;h2&gt;Runner를 사용한 방법&lt;/h2&gt;
&lt;p&gt;callRunners의 동작은 간단하다.&lt;/p&gt;
&lt;p&gt;bean 중에서 &lt;code&gt;ApplicationRunner&lt;/code&gt; , &lt;code&gt;CommandLineRunner&lt;/code&gt; 인터페이스를 상속받은 것이 있다면 모두 실행한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List&amp;lt;Object&amp;gt; runners = new ArrayList&amp;lt;&amp;gt;();

    // ApplicationRunner bean 모두 추가
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());

    // CommandLineRunner bean 모두 추가
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());

    // runners 정렬
    AnnotationAwareOrderComparator.sort(runners);

    for (Object runner : new LinkedHashSet&amp;lt;&amp;gt;(runners)) {
        if (runner instanceof ApplicationRunner) {
            callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            callRunner((CommandLineRunner) runner, args);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행 코드를 보면 실행 전에 정렬을 하고 있다.&lt;/p&gt;
&lt;p&gt;sort 메소드를 찍고 따라가다 보면 아래의 코드가 나타난다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private static Integer findOrder(MergedAnnotations annotations) {
    MergedAnnotation&amp;lt;Order&amp;gt; orderAnnotation = annotations.get(Order.class);
    if (orderAnnotation.isPresent()) {
        return orderAnnotation.getInt(MergedAnnotation.VALUE);
    }
    MergedAnnotation&amp;lt;?&amp;gt; priorityAnnotation = annotations.get(JAVAX_PRIORITY_ANNOTATION);
    if (priorityAnnotation.isPresent()) {
        return priorityAnnotation.getInt(MergedAnnotation.VALUE);
    }
    return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;어노테이션 &lt;code&gt;@Order&lt;/code&gt; 혹은 &lt;code&gt;@Priority&lt;/code&gt;(JAVAX_PRIORITY_ANNOTATION = &lt;b&gt;javax.annotation.Priority)&lt;/b&gt;을 사용하면 그 value를 우선순위로 사용한다.&lt;/p&gt;
&lt;p&gt;단, 두 어노테이션을 모두 사용하면 &lt;code&gt;@Order&lt;/code&gt; 만을 사용한다.&lt;/p&gt;
&lt;h3&gt;ApplicationRunner&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ApplicationRunner&lt;/code&gt; 인터페이스를 상속받고 run 메소드를 작성한다.&lt;/p&gt;
&lt;p&gt;run은 &lt;code&gt;ApplicationArguments&lt;/code&gt; 인터페이스를 인자로 사용하는데, java 실행 시 삽입하는 인자를 추상화하여 사용하는 객체이다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;@Component
public class AppRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        System.out.println(&quot;==========================&quot;);
        System.out.println(&quot;ApplicationRunner: &quot; + Arrays.toString(args.getSourceArgs()));
        System.out.println(&quot;==========================&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 코드를 작성하고 확인을 위해 자바 실행 시 적절한 인자를 넣어본다.&lt;/p&gt;
&lt;p&gt;내가 넣은 인자는 Initializing Spring boot...이다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Intellij을 사용한다면 Run Configuration에 넣어주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;인자.png&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;683&quot; width=&quot;755&quot; height=&quot;NaN&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wmnF2/btqD2w2rc5c/huACKCY99J2yUYiELo48cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wmnF2/btqD2w2rc5c/huACKCY99J2yUYiELo48cK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wmnF2/btqD2w2rc5c/huACKCY99J2yUYiELo48cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwmnF2%2FbtqD2w2rc5c%2FhuACKCY99J2yUYiELo48cK%2Fimg.png&quot; data-filename=&quot;인자.png&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;683&quot; width=&quot;755&quot; height=&quot;NaN&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.7.RELEASE)

2020-05-11 21:50:31.130  INFO 7320 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on **** with PID 7320 (****)
2020-05-11 21:50:31.132  INFO 7320 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-05-11 21:50:31.390  INFO 7320 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.428 seconds (JVM running for 0.802)
==========================
ApplicationRunner: [Initializing, spring, boot...]
==========================

Process finished with exit code 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에서 확인한 것처럼 Spring이 설정이 완료된 후 동작함을 알 수 있다.&lt;/p&gt;
&lt;h3&gt;CommandLineRunner&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ApplicationRunner&lt;/code&gt; 인터페이스와 사용법이 같다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CommandLineRunner&lt;/code&gt; 인터페이스를 상속받고 run 메소드를 오버라이드하면 된다.&lt;/p&gt;
&lt;p&gt;차이점은 인자가 String... 타입이라는 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Component
public class CommandRunner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        System.out.println(&quot;==========================&quot;);
        System.out.println(&quot;CommandLineRunner: &quot; + Arrays.toString(args));
        System.out.println(&quot;==========================&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과도 위와 마찬가지이다.&lt;/p&gt;
&lt;h2&gt;Event를 사용한 방법&lt;/h2&gt;
&lt;p&gt;이벤트는 Spring의 core에 포함된 기능 중 하나이지만 여기서 사용할 이벤트는 boot 패키지에 포함되어 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ApplicationStartedEvent
&lt;ul&gt;
&lt;li&gt;App이 실행된 후에 발생하는 이벤트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ApplicationStartingEvent
&lt;ul&gt;
&lt;li&gt;App 실행 시 발생하는 이벤트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;ApplicationStartedEvent&lt;/h3&gt;
&lt;p&gt;SpringApplication.run() 에서 listeners.started()가 실행되는 시점에서 이벤트가 발생한다.&lt;/p&gt;
&lt;p&gt;Spring event publish하는 방법을 사용해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Component
public class AppStartedEvent implements ApplicationListener&amp;lt;ApplicationStartedEvent&amp;gt; {

    @Override
    public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
        System.out.println(&quot;==========================&quot;);
        System.out.println(&quot;ApplicationStartedEvent: &quot; + Arrays.toString(applicationStartedEvent.getArgs()));
        System.out.println(&quot;==========================&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의도대로 Runner 수행 전 위의 메시지가 나타난다.&lt;/p&gt;
&lt;h3&gt;ApplicationStartingEvent&lt;/h3&gt;
&lt;p&gt;spring에서는 이벤트를 bean으로 처리하지만, &lt;code&gt;ApplicationStartingEvent&lt;/code&gt; 는 앱 실행 초기에 발생하는 이벤트이다.&lt;/p&gt;
&lt;p&gt;문제는 이 이벤트가 context 메타데이터 설정 전에 발생한다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Component
public class AppStartingEvent implements ApplicationListener&amp;lt;ApplicationStartingEvent&amp;gt; {

    @Override
    public void onApplicationEvent(ApplicationStartingEvent applicationStartingEvent) {
        System.out.println(&quot;==========================&quot;);
        System.out.println(&quot;ApplicationStartingEvent: &quot; + Arrays.toString(applicationStartingEvent.getArgs()));
        System.out.println(&quot;==========================&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드는 의도대로 작동하지 않는다.&lt;/p&gt;
&lt;p&gt;이유는 위에 명시한 대로 이벤트 발생 시점 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Component&lt;/code&gt; 를 사용하여 bean으로 등록했지만 이 이벤트는 bean 설정 전에 발생하는 것이다.&lt;/p&gt;
&lt;p&gt;정상 동작을 위해 main 코드를 수정한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //SpringApplication.run(DemoApplication.class, args); //default 시작 구문 삭제
        SpringApplication application = new SpringApplication(DemoApplication.class);
        application.addListeners(new AppStartingEvent());
        application.run(args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AppStartingEvent&lt;/code&gt; 클래스를 인스턴스로 만들고 리스너로 등록한다.&lt;/p&gt;
&lt;p&gt;클래스를 직접 인스턴스로 선언했으므로 &lt;code&gt;@Component&lt;/code&gt; 어노테이션은 의미가 없으니 제거한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;//@Component
public class AppStartingEvent implements ApplicationListener&amp;lt;ApplicationStartingEvent&amp;gt; {

    @Override
    public void onApplicationEvent(ApplicationStartingEvent applicationStartingEvent) {
        System.out.println(&quot;==========================&quot;);
        System.out.println(&quot;ApplicationStartingEvent: &quot; + Arrays.toString(applicationStartingEvent.getArgs()));
        System.out.println(&quot;==========================&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;위의 코드를 모두 포함하고 실행하면 결과는 아래처럼 나타난다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;==========================
ApplicationStartingEvent: [Initializing, spring, boot...]
==========================

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.7.RELEASE)

2020-05-11 22:55:40.619  INFO 10684 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on **** with PID 10684 (****)
2020-05-11 22:55:40.621  INFO 10684 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-05-11 22:55:40.873  INFO 10684 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.418 seconds (JVM running for 0.786)
==========================
ApplicationStartedEvent: [Initializing, spring, boot...]
==========================
==========================
CommandLineRunner: [Initializing, spring, boot...]
==========================
==========================
ApplicationRunner: [Initializing, spring, boot...]
==========================

Process finished with exit code 0&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;인스턴스의 라이프 사이클을 활용한 방법&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@PostConstruct&lt;/code&gt; 을 메소드에 붙이면 생성자 호출 후 바로 실행된다.&lt;/p&gt;
&lt;p&gt;SpringApplication.run()에서 context refresh할 때 싱글턴 타입의 bean 인스턴스를 생성하기 때문에 Spring boot 시작 로그가 끝나기 전에 동작하게 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
public class UsePostConstruct {

    @PostConstruct
    public void init() {
        System.out.println(&quot;==========================&quot;);
        System.out.println(&quot;PostConstruct&quot;);
        System.out.println(&quot;==========================&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자매품으로 &lt;code&gt;@PreDestroy&lt;/code&gt; 가 있다. 인스턴스 파괴 전 실행된다.&lt;/p&gt;
&lt;p&gt;여기까지 모두 포함하면 결과는 아래처럼 나타난다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;==========================
ApplicationStartingEvent: [Initializing, spring, boot...]
==========================

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.7.RELEASE)

2020-05-11 23:08:05.399  INFO 10976 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on **** with PID 10976 (****)
2020-05-11 23:08:05.401  INFO 10976 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
==========================
PostConstruct
==========================
2020-05-11 23:08:05.661  INFO 10976 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.428 seconds (JVM running for 0.79)
==========================
ApplicationStartedEvent: [Initializing, spring, boot...]
==========================
==========================
CommandLineRunner: [Initializing, spring, boot...]
==========================
==========================
ApplicationRunner: [Initializing, spring, boot...]
==========================

Process finished with exit code 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;별거 없지만, 혹시나 풀 코드를 나중에 확인해보고 싶을까봐 업로드해두었다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dhmin5693/spring-initialize&quot;&gt;spring-initialize&lt;/a&gt;&lt;/p&gt;</description>
      <category>Java/Spring framework</category>
      <category>Spring</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/96</guid>
      <comments>https://private-space.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 11 May 2020 23:26:39 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 다양한 연관관계 매핑</title>
      <link>https://private-space.tistory.com/95</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;김영한 님의 자바 ORM 표준 JPA 프로그래밍을 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788960777330&amp;amp;orderClick=LEa&amp;amp;Kc=&quot;&gt;교보문고 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;  다양한 연관관계 매핑&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;엔티티 연관관계 매핑 시 고려할 3가지&lt;ol&gt;
&lt;li&gt;다중성&lt;/li&gt;
&lt;li&gt;단방향/양방향&lt;/li&gt;
&lt;li&gt;연관관계의 주인&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 다중성&lt;/h3&gt;
&lt;p&gt;연관관계는 보통 4가지 다중성을 갖는다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;다대일&lt;/li&gt;
&lt;li&gt;일대다&lt;/li&gt;
&lt;li&gt;일대일&lt;/li&gt;
&lt;li&gt;다대다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;다대일, 일대다 관계를 많이 사용하며 다대다 관계는 거의 사용되지 않는다.&lt;/p&gt;
&lt;h3&gt;2. 단방향/양방향&lt;/h3&gt;
&lt;p&gt;테이블과는 달리 객체는 참조용 필드를 가지며, 그 필드를 통해 연관 객체를 조회한다.&lt;/p&gt;
&lt;p&gt;한 쪽이 다른 한 쪽을 일방적으로 참조하면 단방향이며 서로 참조하면 양방향이다.&lt;/p&gt;
&lt;h3&gt;3. 연관관계의 주인&lt;/h3&gt;
&lt;p&gt;테이블은 외래키 하나로 두 테이블이 연관 관계를 가질 수 있기 때문에 연관관계를 관리하는 포인트는 &lt;em&gt;외래키 하나&lt;/em&gt;이다.&lt;/p&gt;
&lt;p&gt;엔티티가 양방향으로 참조되면 A → B, B → A 둘 중 하나를 정하여 외래키를 관리해야 한다.&lt;/p&gt;
&lt;p&gt;보통은 &lt;strong&gt;외래키를 가진 쪽&lt;/strong&gt;을 연관관계의 주인으로 선택한다.&lt;/p&gt;
&lt;p&gt;주인이 아닌 방향은 읽기만 가능하다.&lt;/p&gt;
&lt;p&gt;연관관계의 주인은 &lt;code&gt;mappedBy&lt;/code&gt; 속성을 사용할 수 없으며, 주인이 아닌 쪽에 &lt;code&gt;mappedBy&lt;/code&gt; 로 주인 필드(외래키)를 지정한다.&lt;/p&gt;
&lt;h2&gt;  다대일&lt;/h2&gt;
&lt;p&gt;다대일과 일대다는 항상 반대 방향에 존재한다.&lt;/p&gt;
&lt;p&gt;외래키는 항상 &lt;strong&gt;다&lt;/strong&gt;쪽에 있기 때문에 연관관계의 주인은 항상 다쪽이다.&lt;/p&gt;
&lt;p&gt;회원(N)과 팀(1)이 있으면 회원이 연관관계의 주인이다.&lt;/p&gt;
&lt;h3&gt;  다대일 단방향&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;회원 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Member {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     @ManyToOne
     @JoinColumn(name = &amp;quot;TEAM_ID&amp;quot;)
     private Team team;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;팀 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Team {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;TEAM_ID&amp;quot;)
     private Long id;

     private String name;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;회원은 &lt;code&gt;Member.team&lt;/code&gt; 으로 참조가 가능하지만, 팀에선 회원을 참조할 필드가 없어 &lt;strong&gt;단방향&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;테이블로 구성한다면 외래키는 &lt;code&gt;Member&lt;/code&gt;에만 존재한다.&lt;/p&gt;
&lt;h3&gt;  다대일 양방향&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;회원 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Member {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     @ManyToOne
     @JoinColumn(name = &amp;quot;TEAM_ID&amp;quot;)
     private Team team;

     public void setTeam(Team team) {
         this.team = team;

         // 무한루프 방지
         if (!team.getMembers().contains(this)) {
             team.getMembers().add(this);
         }
     }
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;팀 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Team {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;TEAM_ID&amp;quot;)
     private Long id;

     private String name;

     @OneToMany(mappedBy = &amp;quot;team&amp;quot;)
     private List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();

     public void addMember(Member member) {
         this.members.add(member);
         // 무한루프 방지
         if (member.getTeam() != this) {
             member.setTeam(this);
         }
     }
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;팀 엔티티에 List가 추가되었고, 주인 필드인 &lt;code&gt;Member.team&lt;/code&gt;을 가르키고 있다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Team.members&lt;/code&gt; 는 주인이 아니므로 조회가 필요할 때 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;양방향 연관관계는 항상 서로를 참조해야 함에 주의한다.&lt;ul&gt;
&lt;li&gt;setTeam, addMember는 서로를 참조할 때 무한루프에 빠지지 않도록 처리되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  일대다&lt;/h2&gt;
&lt;h3&gt;  일대다 단방향&lt;/h3&gt;
&lt;p&gt;하나의 팀이 여러 팀원을 참조할 수 있는 경우가 일대다 단방향 관계이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;팀 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Team {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;TEAM_ID&amp;quot;)
     private Long id;

     private String name;

     @OneToMany
     @JoinColumn(name = &amp;quot;TEAM_ID&amp;quot;)
     private List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;회원 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Member {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;팀 엔티티의 &lt;code&gt;@JoinColumn&lt;/code&gt; 을 보면 일대다인 경우에도 N인 쪽에 외래키가 존재함을 알 수 있다.&lt;/p&gt;
&lt;p&gt;엔티티가 관리하는 테이블과 다른 테이블에 외래키가 있기 때문에 insert 시 문제가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1) member 1, 2 생성
2) team 1 생성 및 member 1, 2의 소속을 team 1로 옮김.
3) member 1, 2, team 1 저장
    - member 1, 2의 저장엔 문제가 없다.
  - team 1 저장 시 team 1을 참조하는 member의 정보가 team 엔티티 내부에 없다.
  - 따라서, JPA는 update Member set TEAM_ID = ? where MEMBER_ID를 한번 더 실행할 수밖에 없다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이러한 문제는 쿼리 성능에 문제를 끼치며 관리도 어려워진다.&lt;/p&gt;
&lt;p&gt;일대다 대신 &lt;strong&gt;다대일&lt;/strong&gt;을 사용하는 것을 권장한다.&lt;/p&gt;
&lt;h3&gt;  일대다 양방향&lt;/h3&gt;
&lt;p&gt;다대일 양방향과 같은 말이지만 JPA에선 약간의 차이가 있다.&lt;/p&gt;
&lt;p&gt;일단. 외래키는 무조건 다쪽에 존재해야 한다는 점엔 변화가 없다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;팀 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Team {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;TEAM_ID&amp;quot;)
     private Long id;

     private String name;

     @OneToMany
     @JoinColumn(name = &amp;quot;TEAM_ID&amp;quot;)
     private List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;회원 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Member {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     @ManyToOne
     @JoinColumn(name = &amp;quot;TEAM_ID&amp;quot;, insertable = false, updatable = false)
     private Team team;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;다대일은 읽기 전용 필드 &lt;code&gt;Team&lt;/code&gt; 이 추가되어 있다(insertable, updatable).&lt;/p&gt;
&lt;p&gt;일대다 단방향 매핑이 갖는 성능, 관리 측면 문제가 그대로 존재하므로 웬만하면 다대일 단방향을 사용하는 방법이 권장된다.&lt;/p&gt;
&lt;h2&gt;  일대일&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;일대일 관계는 반대도 일대일이다.&lt;/li&gt;
&lt;li&gt;일대일은 어느 곳이든 외래 키를 가질 수 있다.&lt;/li&gt;
&lt;li&gt;외래키 하나만 있으면 양쪽 조회가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;외래키 소유 전략은 2가지이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;주 테이블이 외래키 소유&lt;ul&gt;
&lt;li&gt;외래키를 객체 참조처럼 쓸 수 있어서 객체 지향 개발에 편리하다.&lt;/li&gt;
&lt;li&gt;주 테이블이 외래키를 가지므로 주 테이블만 확인해도 대상 테이블과 연관 관계를 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대상 테이블이 외래키 소유&lt;ul&gt;
&lt;li&gt;일반적인 DB 개발자들은 이 방법을 선호한다.&lt;/li&gt;
&lt;li&gt;테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조가 그대로 유지된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;회원이 하나의 라커룸을 가지는 예시로 일대일 관계를 알아본다.&lt;/p&gt;
&lt;p&gt;주 테이블은 회원, 대상 테이블은 라커룸이다.&lt;/p&gt;
&lt;h3&gt; ‍♂️ 주 테이블에 외래키&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;회원 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Member {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     @OneToOne
     @JoinColumn(name = &amp;quot;LOCKER_ID&amp;quot;)
     private Locker locker;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;라커룸 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Locker {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;LOCKER_ID&amp;quot;)
     private Long id;

     private String name;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;주 테이블인 회원에 &lt;code&gt;Locker&lt;/code&gt; 필드(외래키)가 포함되어 있다.&lt;/p&gt;
&lt;h3&gt; ‍♂️ 양방향&lt;/h3&gt;
&lt;p&gt;라커룸 엔티티를 약간만 바꾸어본다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Entity
public class Locker {

    @Id
    @GeneratedValue
    @Column(name = &amp;quot;LOCKER_ID&amp;quot;)
    private Long id;

    private String name;

    @OneToOne(mappedBy = &amp;quot;member&amp;quot;)
    private Member member;
    ...
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;주인 필드인 회원 엔티티가 외래키를 가지므로 주인 필드를 나타내는 &lt;code&gt;mappedBy&lt;/code&gt; 속성과 &lt;code&gt;@OneToOne&lt;/code&gt; 어노테이션을 추가했다.&lt;/p&gt;
&lt;h3&gt; ‍♀️ 대상 테이블에 외래키&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;JPA 2.0 이상&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;JPA 2.0부터 대상 테이블에 외래키가 있는 매핑이 지원된다.&lt;/p&gt;
&lt;p&gt;위 예시와 반대로 &lt;code&gt;Locker&lt;/code&gt;에 &lt;code&gt;Member&lt;/code&gt; 필드를 추가하면 된다.&lt;/p&gt;
&lt;h3&gt; ‍♀️ 양방향&lt;/h3&gt;
&lt;p&gt;위 예시와 반대로 &lt;code&gt;Member.locker&lt;/code&gt; 가 주인 필드인 &lt;code&gt;Locker.member&lt;/code&gt; 를 가리키면 된다.&lt;/p&gt;
&lt;h2&gt; ‍ ‍ ‍  다대다&lt;/h2&gt;
&lt;p&gt;RDBMS는 정규화된 2개의 테이블로 다대다 관계를 표현할 수 없으며 두 테이블을 연결하는 별도의 테이블이 필요하다.&lt;/p&gt;
&lt;p&gt;회원과 상품이 다대다의 관계라고 하면 먼저 회원_상품 테이블을 생성한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;회원 : 회원_상품 = 1:N&lt;/li&gt;
&lt;li&gt;회원_상품 : 상품 = M:1&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;객체는 테이블과는 다르게 &lt;code&gt;@ManyToMany&lt;/code&gt; 를 사용하여 다대다 관계를 깔끔하게 만들 수 있다.&lt;/p&gt;
&lt;h3&gt; ‍ ‍  다대다 단방향&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;회원 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Member {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     @ManyToMany
     @JoinTable(name = &amp;quot;MEMBER_PRODUCT&amp;quot;,
                 joinColumns = @JoinColumn(name = &amp;quot;MEMBER_ID&amp;quot;),
                 inverseJoinColumns = @JoinColumn(name = &amp;quot;PRODUCT_ID&amp;quot;))
     private List&amp;lt;Product&amp;gt; products = new ArrayList&amp;lt;&amp;gt;();

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;상품 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Product {

     @Id
     @Column(name = &amp;quot;PRODUCT_ID&amp;quot;)
     private String id;

     private String name;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;회원 엔티티에는 &lt;code&gt;@ManyToMany&lt;/code&gt; 로 다대다 매핑 처리가 되어있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@JoinTable&lt;/code&gt; 은 연결 테이블을 매핑하는 어노테이션이다.&lt;/p&gt;
&lt;p&gt;속성을 정리해보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;name&lt;ul&gt;
&lt;li&gt;연결할 별도의 테이블 이름을 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;joinColumns&lt;ul&gt;
&lt;li&gt;현재 방향인 회원 엔티티를 기준으로, 매핑할 조인 컬럼 정보를 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;inverseJoinColumns&lt;ul&gt;
&lt;li&gt;반대 방향인 상품 엔티티를 기준으로, 매핑할 조인 컬럼 정보를 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;회원_상품 테이블은 &lt;code&gt;@JoinTable&lt;/code&gt; 의 name 속성에서만 존재할 뿐, 접근할 객체는 존재하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Product product1 = new Product();
product1.setId(&amp;quot;product1&amp;quot;);
product1.setName(&amp;quot;상품1&amp;quot;);
em.persist(product1);

Member member1 = new Member();
member1.setId(&amp;quot;member1&amp;quot;);
member1.setUsername(&amp;quot;회원1&amp;quot;);
member1.getProducts().add(product1);
em.persist(member1);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;실제로 코드를 작성할 때도 회원_상품 테이블을 전혀 신경쓰지 않고 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;member.getProducts()&lt;/code&gt; 를 호출할 때 JPA가 해석하여 실제 테이블에 사용할 쿼리를 확인하면 &lt;strong&gt;MEMBER_PRODUCT&lt;/strong&gt;이 FROM 절에 포함된다.&lt;/p&gt;
&lt;h3&gt; ‍ ‍  다대다 양방향&lt;/h3&gt;
&lt;p&gt;다른 양방향과 마찬가지로 역방향도 &lt;code&gt;@ManyToMany&lt;/code&gt; 를 지정하고, &lt;code&gt;mappedBy&lt;/code&gt;로 주인 필드를 지정한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;상품 엔티티에 역방향 참조 추가&lt;/p&gt;
&lt;p&gt;  @Entity&lt;br&gt;  public class Product {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Id
  @Column(name = &amp;quot;PRODUCT_ID&amp;quot;)
  private String id;

  private String name;

  @ManyToMany(mappedBy = &amp;quot;products&amp;quot;)
  private List&amp;lt;Member&amp;gt; members;

  ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;  }&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt; 다대다 매핑 한계와 극복&lt;/h3&gt;
&lt;p&gt;실제 업무를 하다 보면 회원_상품 테이블은 단순히 연결을 위해서 양쪽 외래키만 포함하는 테이블로 만들지는 않는다.&lt;/p&gt;
&lt;p&gt;이를테면 &lt;strong&gt;상품 주문 시간&lt;/strong&gt;이나 &lt;strong&gt;주문량&lt;/strong&gt; 같은 정보가 포함될 수 있다.&lt;/p&gt;
&lt;p&gt;그렇게 되면 위에서 &lt;code&gt;@ManyToMany&lt;/code&gt; 를 사용한 예제는 사용할 수 없기 때문에 별도의 엔티티를 만들어야 한다.&lt;/p&gt;
&lt;p&gt;추가적으로 회원과 회원_상품, 회원_상품과 상품의 관계를 별도로 정의한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;회원 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Member {

     @Id
     @GeneratedValue
     @Column(name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     @OneToMany(mappedBy = &amp;quot;member&amp;quot;)
     private List&amp;lt;MemberProduct&amp;gt; emberProducts = new ArrayList&amp;lt;&amp;gt;();

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;상품 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Product {

     @Id
     @Column(name = &amp;quot;PRODUCT_ID&amp;quot;)
     private String id;

     private String name;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;회원_상품 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 @IdClass(MemberProductId.class)
 public class MemberProduct {

     @Id
     @ManyToOne
     @JoinColumn(name = &amp;quot;MEMBER_ID&amp;quot;)
     private Member member;

     @Id
     @ManyToOne
     @JoinColumn(name = &amp;quot;PRODUCT_ID&amp;quot;)
     private Product product;

     private Date orderAmount;

     ...
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;MemberProductId&lt;/code&gt; 정의&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @NoArgsConstructor
 @EqualsAndHashCode
 public class MemberProductId implements Serializable {
     private String member;
     private String product;
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;회원_상품 엔티티에는 &lt;code&gt;@IdClass&lt;/code&gt; 어노테이션이 포함된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@IdClass&lt;/code&gt; 는 복합키를 사용하겠다는 의미이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MemberProductId&lt;/code&gt; 의 각 필드는 &lt;code&gt;MemberProduct&lt;/code&gt; 엔티티의 &lt;code&gt;Member&lt;/code&gt; 필드와 &lt;code&gt;Product&lt;/code&gt; 필드에 매핑된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;복합키&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;복합키는 별도의 클래스를 정의해야 하며(&lt;code&gt;MemberProductId&lt;/code&gt;), 이 클래스는 public으로 선언되어야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Serializable&lt;/code&gt; 을 구현해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Equals, Hashcode를 재정의해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;기본 생성자가 있어야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@EmbeddedId&lt;/code&gt; 를 사용하여 정의하는 방법도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;회원_상품의 예시처럼 외래키를 자신의 기본키로 사용하는 것을 &lt;strong&gt;식별관계&lt;/strong&gt;라고 한다.&lt;/p&gt;
&lt;p&gt;// 데이터 저장하기&lt;/p&gt;
&lt;p&gt;Member m1 = new Member();&lt;br&gt;// m1 데이터 설정&lt;br&gt;em.persist(member1);&lt;/p&gt;
&lt;p&gt;Product p1 = new Product();&lt;br&gt;// p1 데이터 설정&lt;br&gt;em.persist(p1);&lt;/p&gt;
&lt;p&gt;MemberProduct mp = new MemberProduct();&lt;br&gt;mp.setMember(m1);&lt;br&gt;mp.setProduct(p1);&lt;br&gt;...&lt;/p&gt;
&lt;p&gt;em.persist(mp);&lt;/p&gt;
&lt;p&gt;MemberProductId mpId = new MemberProductId();&lt;br&gt;// mpId 데이터 설정&lt;/p&gt;
&lt;p&gt;MemberProduct mp = em.find(MemberProduct.class, mpId);&lt;br&gt;Member m1 = mp.getMember();&lt;br&gt;Product p1 = mp.getProduct();&lt;br&gt;...&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt; 다대다 테이블의 기본 키를 새로 정의하기&lt;/h3&gt;
&lt;p&gt;복합 키를 사용하면 식별자 클래스도 새로 정의해야 하는 등 굉장히 번거롭다.&lt;/p&gt;
&lt;p&gt;반면 DB에서 자동 생성하는 Long 타입 대리키를 사용하면 간편하게 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;회원_상품 테이블을 주문이라고 다시 명명하고 새로 작성한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;주문 엔티티&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Entity
  public class Order {

      @Id
      @GeneratedValue
      @Column(name = &amp;quot;ORDER_ID&amp;quot;)
      private Long id;

      @ManyToOne
      @JoinColumn(name = &amp;quot;MEMBER_ID&amp;quot;)
      private Member member;

      @ManyToOne
      @JoinColumn(name = &amp;quot;PRODUCT_ID&amp;quot;)
      private Product product;

      private Date orderAmount;

      ...
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;MemberProduct&lt;/code&gt;를 재정의한 &lt;code&gt;Order&lt;/code&gt; 는 식별 관계가 사라졌다.&lt;/p&gt;
&lt;p&gt;다대다 테이블은 복합키를 사용한 식별 관계, 대리키를 사용한 비식별 관계 중 적절한 방법을 선택하여 구현할 수 있다.&lt;/p&gt;</description>
      <category>Java/JPA</category>
      <category>JPA</category>
      <category>다대다</category>
      <category>다대일</category>
      <category>단방향</category>
      <category>양방향</category>
      <category>연관관계</category>
      <category>일대다</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/95</guid>
      <comments>https://private-space.tistory.com/95#entry95comment</comments>
      <pubDate>Tue, 7 Apr 2020 01:07:43 +0900</pubDate>
    </item>
    <item>
      <title>DDD 도메인 모델의 군집, 애그리거트(Aggregate)</title>
      <link>https://private-space.tistory.com/94</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788993827446&amp;amp;orderClick=LAG&amp;amp;Kc=&quot;&gt;최범균 님의 DDD Start&lt;/a&gt;를 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;DDD 애그리거트(Aggregate)&lt;/h1&gt;
&lt;p&gt;테이블이 100개 이상 있는 ERD를 보고 있다고 생각해보자.&lt;/p&gt;
&lt;p&gt;하나 하나 따라가보면 개별 테이블의 연관 관계는 알 수는 있지만, 한 눈에 전체의 구조를 파악하기는 굉장히 어렵다.&lt;/p&gt;
&lt;p&gt;도메인 모델도 마찬가지이다. 그렇게 되면 수정사항이 생겼을 때 코드를 변경하고 확장하는 일이 매우 힘들어질 것이다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;애그리거트&lt;/b&gt;를 활용하면 이러한 어려움을 해결할 수 있다.&lt;/p&gt;
&lt;h2&gt; 애그리거트&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;애그리거트는 관련 도메인을 하나의 군집으로 묶은 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;aggregate.png&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DiDxi/btqDebDnTFY/vGywOniCSiJPc9SVcPwU0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DiDxi/btqDebDnTFY/vGywOniCSiJPc9SVcPwU0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DiDxi/btqDebDnTFY/vGywOniCSiJPc9SVcPwU0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDiDxi%2FbtqDebDnTFY%2FvGywOniCSiJPc9SVcPwU0K%2Fimg.png&quot; data-filename=&quot;aggregate.png&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;557&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;애그리거트를 사용하면 연관 도메인을 묶어서 이해하기 때문에 모델 관계를 파악하기가 더 쉽다.&lt;/p&gt;
&lt;p&gt;또한 더 잘 이해할 수 있고 애그리거트 단위로 일관성을 관리하면 코드도 일목조연하게 작성할 수 있다.&lt;/p&gt;
&lt;p&gt;코드의 복잡도가 낮아지기 때문에 유지보수 및 확장, 변경에 들이는 노력이 줄어든다.&lt;/p&gt;
&lt;h2&gt; 애그리거트 루트&lt;/h2&gt;
&lt;p&gt;애그리거트에 속한 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;루트 엔티티&lt;/b&gt;는 애그리거트의 대표 엔티티로, 애그리거트에 속한 엔티티는 루트 엔티티에 직접 혹은 간접적으로 속한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 조율하는 것이다.&lt;/li&gt;
&lt;li&gt;애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 제공한다.
&lt;ul&gt;
&lt;li&gt;주문 애그리거트의 루트 엔티티 Order는 관련 기능을 구현한 메소드를 제공
&lt;ol&gt;
&lt;li&gt;배송지 변경&lt;/li&gt;
&lt;li&gt;상품 변경 등.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt; 일관성이 깨지는 경우&lt;/h2&gt;
&lt;p&gt;주문 애그리거트의 루트인 &lt;code&gt;Order&lt;/code&gt; 는 배송 정보 &lt;code&gt;ShippingInfo&lt;/code&gt; 를 갖는다.&lt;/p&gt;
&lt;p&gt;배송지 주소는 배송 중이거나 배송 완료인 경우 변경이 불가능하다.&lt;/p&gt;
&lt;p&gt;하지만 아래 코드는 개발자가 어디든 집어 넣기만 한다면 규칙에 상관없이 배송지를 변경할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;ShippingInfo info = order.getShippingInfo();
info.setAddress(newAddress);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;검사 로직을 추가하여 함부로 변경하지 못하도록 제한할 수도 있다.&lt;/p&gt;
&lt;p&gt;그런 경우 다른 위치에서 중복된 코드를 작성할 가능성이 매우 높아지게 된다.&lt;/p&gt;
&lt;h2&gt; 일관성을 지키려면?&lt;/h2&gt;
&lt;p&gt;일관성을 지키는 가장 좋은 방법은 &lt;b&gt;일관성을 지키도록 강제&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p&gt;두 가지 습관을 들여 두면 일관성을 지키는데 큰 도움이 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;public setter를 만들지 않는다.&lt;/li&gt;
&lt;li&gt;밸류 타입은 불변으로 만든다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;public Setter 만들지 않기&lt;/h3&gt;
&lt;p&gt;public setter는 일반적으로 필드에 값을 할당하기 위해 사용한다.&lt;/p&gt;
&lt;p&gt;습관처럼 작성하지만 public setter는 도메인의 의미를 적절하게 표현할 수 없다.&lt;/p&gt;
&lt;p&gt;또한 public setter가 포함된 객체는 단순히 특정 필드를 묶은 것처럼 이해될 수 있으며 그렇게 개발할 가능성이 높아진다.&lt;/p&gt;
&lt;p&gt;그렇게 개발된 로직은 보통 한 곳에 응집되지 않고 응용 영역/표현 영역으로 분산되어 유지보수 시 분석 및 수정에 더 많은 시간을 들여야 한다.&lt;/p&gt;
&lt;p&gt;setter를 사용하지 않는다면 의미가 드러나는 메소드 명을 고민하게 되고, 이에 걸맞는 로직을 응집시켜 구현하게 된다.&lt;/p&gt;
&lt;h3&gt;밸류 타입 불변으로 만들기&lt;/h3&gt;
&lt;p&gt;public setter 만들지 않기의 연장 선상이다.&lt;/p&gt;
&lt;p&gt;애그리거트 루트에서 밸류 객체를 구한다 해도 값이 변경되지 않는다면, 애그리거트의 외부에서 밸류 객체 상태를 함부로 바꿀 수 없다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ShippingInfo info = order.getShippingInfo();
info.setAddress(newAddress); // address가 불변이면 외부에서 이 로직을 사용할 수 없다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;대신 루트를 통해 애그리거트 정보를 바꾸어주자.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class Order {
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt; 애그리거트 기능 구현&lt;/h2&gt;
&lt;p&gt;애그리거트 루트는 애그리거트 내부의 다른 객체를 조합하여 기능을 완성해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Order&lt;/code&gt; 는 총 주문 금액을 구하기 위해 &lt;code&gt;OrderLine&lt;/code&gt; 목록을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;public class Order {
    private Money totalAmounts;
    private List&amp;lt;OrderLine&amp;gt; orderLines;

    private void calculateTotalAmounts() {
        int sum = orderLines.stream()
                .mapToInt(orderLine -&amp;gt; orderLine.getPrice() * orderLine.quantity())
                .sum();

        this.totalAmounts = new Money(sum);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt; 트랜잭션의 범위&lt;/h2&gt;
&lt;p&gt;한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;성능 문제
&lt;ul&gt;
&lt;li&gt;두 개 이상의 애그리거트를 수정하면 트랜잭션 충돌이 발생할 가능성이 높아진다.&lt;/li&gt;
&lt;li&gt;또한 트랜잭션이 잠궈둬야 하는 행이 많아진다.&lt;/li&gt;
&lt;li&gt;애그리거트가 하나인 경우 한 행을 잠그고, 여러 개인 경우 그만큼 늘어나게 된다.&lt;/li&gt;
&lt;li&gt;당연히 성능에 문제가 생길 가능성이 높아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;독립성이 깨진다
&lt;ul&gt;
&lt;li&gt;다른 애그리거트에 의존하면 애그리거트 간 결합도가 매우 높아진다(⛓⛓⛓).&lt;/li&gt;
&lt;li&gt;결합도가 높아지면 수정이 매우 어려워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;아래 코드는 &lt;code&gt;Order&lt;/code&gt; 에서 &lt;code&gt;Customer&lt;/code&gt; 의 주소를 변경하고 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class Order {
    private Orderer orderer;

    public void shipTo(ShippingInfo shippingInfo, boolean useNewShippingAddrAsMemberAddr) {
        verifyNotYetShipped();
        setShippingInfo(newShipingInfo);

        if (useNewShippingAddrAsMemberAddr) {
            // 다른 애그리거트 Customer를 수정하고 있다.
            orderer.getCustomer().changeAddress(newShippingInfo.getAddress());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 기능을 수행할 별도의 서비스 객체를 생성하는 것이 바람직하다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class ChangeOrderService {

    @Transactional
    public void changeShippingInfo(...) {
        Order order = orderRepository.findById(id);
        if (order == null) {
            throw new OrderNotFoundException();
        }

        order.shipTo(newShippingInfo);
        if (useNewShippingAddrAsMemberAddr) {
            order.getOrderer().getCustomer().changeAddress(newShippingInfo.getAddress());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한 트랜잭션에서 1개의 애그리거트를 변경하는 것은 필수가 아닌 권장사항이다.&lt;/p&gt;
&lt;p&gt;상황에 맞추어 두 개 이상의 애그리거트를 수정하는 것을 고려할 수 있다.&lt;/p&gt;
&lt;h2&gt; 레포지토리&lt;/h2&gt;
&lt;p&gt;애그리거트는 한 개의 완전한 도메인 모델을 표현하므로, 레포지토리는 애그리거트 단위로 존재한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Order&lt;/code&gt; 와 &lt;code&gt;OrderLine&lt;/code&gt; 을 별도의 테이블에 저장해도 애그리거트에 포함된 모든 도메인 모델은 루트 엔티티를 위해 존재하고 루트 엔티티에 속하기 때문에 레포지토리는 하나를 사용하는 것이 바람직하다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;// 애그리거트 전체를 영속화하여 저장한다.
orderRepository.save(order);

// 레포지터리는 완전한 order를 제공해야 한다.
Order order = orderRepository.findById(orderId);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt; 애그리거트 참조&lt;/h2&gt;
&lt;p&gt;JPA를 사용하면 필드에 포함하기만 해도 다른 애그리거트를 참조할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class Orderer {
    private Member member;
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;편리한 방법이지만 몇 가지 문제를 안고 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;편한 탐색 오용&lt;/li&gt;
&lt;li&gt;성능 문제&lt;/li&gt;
&lt;li&gt;확장의 어려움&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;편한 탐색 오용&lt;/h3&gt;
&lt;p&gt;편리함을 오용하면 구현의 편리함 때문에 다른 애그리거트를 수정하려는 시도를 할 수 있다.&lt;/p&gt;
&lt;p&gt;다른 애그리거트를 수정하면 의존 결합도가 높아지기 때문에 유지보수 비용이 높아지는 것을 항상 염두에 두도록 하자.&lt;/p&gt;
&lt;h3&gt;성능 문제&lt;/h3&gt;
&lt;p&gt;JPA를 사용하면 lazy loading, eager loading 옵션을 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;연관 객체 조회 즉시 View에 넘겨주어야 한다면 eager loading이 더 유리하겠지만,&lt;/p&gt;
&lt;p&gt;애그리거트의 상태를 변경하는 중에는 불필요 정보는 가져오지 않는 lazy loading이 유리하다.&lt;/p&gt;
&lt;p&gt;경우의 수를 고려하여 로딩 전략을 결정하자.&lt;/p&gt;
&lt;h3&gt;확장의 어려움&lt;/h3&gt;
&lt;p&gt;사용자가 많아지면 단일 DBMS로 시작했던 서비스에 변화가 생기기 시작한다.&lt;/p&gt;
&lt;p&gt;부하 분산을 위해 하위 도메인 별로 시스템을 분리하는 마이크로 서비스 아키텍처를 적용하고,&lt;/p&gt;
&lt;p&gt;각 아키텍처마다 다른 DBMS를 사용할 수도 있다.&lt;/p&gt;
&lt;p&gt;이렇게 되면 JPA 단일 기술로 다른 애그리거트를 참조할 수 없다.&lt;/p&gt;
&lt;h3&gt;해결책&lt;/h3&gt;
&lt;p&gt;필드에 대상 애그리거트를 두지 않고 ID를 두어 간접 참조를 활용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class Orderer {
    private MemberId memberId;
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 되면 모든 객체가 참조로 연결되지 않으며 다른 부과 효과를 불러온다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;애그리거트의 경계가 더 명확해진다.&lt;/li&gt;
&lt;li&gt;애그리거트 간 물리적인 연결이 제거되어 모델 복잡도가 낮아진다.&lt;/li&gt;
&lt;li&gt;애그리거트 간 의존성이 제거되어 응집도가 높아진다.&lt;/li&gt;
&lt;li&gt;구현 복잡도가 낮아지고 성능에 대한 고민을 할 필요가 없다. (로딩 전략이 고정되기 때문)&lt;/li&gt;
&lt;li&gt;애그리거트 별로 다른 기술을 사용할 수 있다. (RDBMS, NoSQL 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 해결책에도 문제점은 존재한다.&lt;/p&gt;
&lt;p&gt;모든 로딩 전략이 lazy loading으로 고정되어 버리는데, 이 전략은 N+1 문제라는 대표적인 문제를 갖고 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Customer customer = customerRepository.findById(ordererId);
List&amp;lt;Order&amp;gt; orders = orderRepository.findByOrderer(ordererId);
List&amp;lt;OrderView&amp;gt; dtos = orders.stream()
                .map(order -&amp;gt; {
                    ProductId id = order.getOrderLines.get(0).getProductId();
                    Product product = productRepository.findById(id);
                    return new OrderView(order, customer, product);
                }).collect(toList());&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드는 각 주문마다 첫 번째 주문 상품 정보를 불러온다.&lt;/p&gt;
&lt;p&gt;N번 조회하는 각 정보에 1번씩 쿼리를 추가로 실행하는 것이다.&lt;/p&gt;
&lt;p&gt;만약 조인을 사용한다면 이 문제는 사라진다.&lt;/p&gt;
&lt;p&gt;ID 참조 방식을 사용하면서 N+1 문제도 방지하고 싶다면 전용 조회 쿼리를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;별도의 Dao 객체를 만들고 JPQL/MyBatis 등 쿼리를 직접 활용하는 기술로 두 테이블을 조인하여 한 번에 불러오는 것이 성능 낭비를 막는 좋은 방법이다.&lt;/p&gt;
&lt;h2&gt;애그리거트 간 집합 연관&lt;/h2&gt;
&lt;p&gt;1:N, N:M 연관 관계에서도 성능에 더 유리한 관계가 있다.&lt;/p&gt;
&lt;p&gt;다음의 두 가지를 고려해보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Category&lt;/code&gt; 에 N개의 &lt;code&gt;Product&lt;/code&gt; 를 포함&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; public class Category {
     private Set&amp;lt;Product&amp;gt; products;
     ...
 }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;이 경우 &lt;code&gt;Category&lt;/code&gt; 에 속한 &lt;code&gt;Product&lt;/code&gt; 를 보여주려면 모든 &lt;code&gt;Product&lt;/code&gt; 를 로딩한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Product&lt;/code&gt; 의 개수가 많으면 성능에 문제를 일으킬 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Product&lt;/code&gt; 에 1개의 &lt;code&gt;Category&lt;/code&gt; 를 포함&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; public class Product {
     private CategoryId categoryId;
     ...
 }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;상품 입장에서 자신이 속한 &lt;code&gt;Category&lt;/code&gt; 를 로딩한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;애그리거트를 팩토리로 활용&lt;/h2&gt;
&lt;p&gt;일부 도메인 로직이 응용 서비스에 노출되는 경우가 있다.&lt;/p&gt;
&lt;p&gt;팩토리 애그리거트를 활용하여 결합도를 낮출 수 있다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;public class RegisterProductService {
    public ProductId registerNewProduct(NewProductRequest request) {
        Store account = accountRepository.findByStoreId(request.getStoreId());
        if (account.isBannedStore()) {
            throw new BannedStoreException();
        }
        ProductId id = productRepository.nextId();
        Product product = new Product(id, ...);
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;일부 기능을 &lt;code&gt;Store&lt;/code&gt; 로 옮겨보자.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;public class Store {
    public Product createProduct(ProductId newProductId, ...) {
        if (isBannedStore) {
            throw new BannedStoreException();
        }
        return new Product(newProductId, ...);
    }
}

public class RegisterProductService {
    public ProductId registerNewProduct(NewProductRequest request) {
        Store account = accountRepository.findByStoreId(request.getStoreId());
        ProductId id = productRepository.nextId();
        // 차단 여부가 응용 서비스에 노출되지 않는다.
        Product product = account.createProduct(id, ...);
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서비스 로직의 방향성은 그대로지만 일부 도메인 로직이 노출되지 않도록 변경되었다.&lt;/p&gt;</description>
      <category>Java/Domain Driven Design</category>
      <category>Aggregate</category>
      <category>domain</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/94</guid>
      <comments>https://private-space.tistory.com/94#entry94comment</comments>
      <pubDate>Mon, 6 Apr 2020 01:50:05 +0900</pubDate>
    </item>
    <item>
      <title>AWS S3 정리 및 AWS Java SDK 활용한 예제 코드</title>
      <link>https://private-space.tistory.com/93</link>
      <description>&lt;h1&gt;Amazon Web Service의 무제한 스토리지, S3&lt;/h1&gt;
&lt;h2&gt;S3 (Simple Storage Service)&lt;/h2&gt;
&lt;p&gt;AWS의 스토리지 특화 서비스.&lt;/p&gt;
&lt;p&gt;생성 즉시 용량 한도 없는 스토리지가 할당되며 사용한 만큼 금액을 지불하여 사용한다.&lt;/p&gt;
&lt;h3&gt;EC2와의 비교&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;EC2는 VM Computing Machine으로, 일반적인 PC 1대의 컴퓨팅 파워를 제공하고 있다.&lt;ul&gt;
&lt;li&gt;인스턴스를 새로 만들 때 정해진 양 만큼의 스토리지를 할당한다.&lt;/li&gt;
&lt;li&gt;스토리지를 더 추가하고 싶다면 고정된 양의 스토리지인 EBS를 추가하고 마운트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;S3는 데이터 저장에 특화된 서비스이다. 용량 제한이 없는 스토리지를 생성하여 사용하기 때문에 EC2처럼 추가 스토리지 할당/마운트 작업이 불필요하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;구성요소&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;버킷&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터를 저장하는 &lt;code&gt;컨테이너&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;버킷 하나가 하나의 저장소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;객체&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;S3에 저장되는 기본 개체&lt;/li&gt;
&lt;li&gt;객체 잠금 기능을 사용하면 &lt;code&gt;write-once-read-many&lt;/code&gt; 정책을 적용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;키&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;버킷 내 &lt;code&gt;객체의 고유 식별자&lt;/code&gt;로, 객체는 반드시 하나의 키를 가져야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;버킷 + 키 + 버전&lt;/code&gt;으로 여러 버킷에서 고유한 파일을 식별할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  http://doc.s3.amazonaws.com/2006-03-01/AmazonS3.swdl
  위의 URL에서 버킷명은 doc이며 2006-03-01/AmazonS3.swdl은 키이다.&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;리전&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;버킷의 지리적 위치&lt;/li&gt;
&lt;li&gt;사용자가 객체를 다른 위치로 전송하지 않는 한 해당 리전을 벗어나지 않는다.&lt;/li&gt;
&lt;li&gt;리전과 리전 사이 data replication을 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;스토리지 클래스&lt;/h2&gt;
&lt;p&gt;S3는 저장소의 활용 목적에 따라 각기 다른 과금 정책을 부여한다.&lt;/p&gt;
&lt;p&gt;저장소가 어떻게 활용되는지를 분석하고 적절한 클래스를 선택해야 비용을 효과적으로 절감할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Standard&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;클라우드 애플리케이션, 컨텐츠 배포, 게임, 빅데이터 분석 등 다용도로 사용 가능&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;짧은 지연 시간, 많은 처리량&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;요금&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스토리지 요금&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;standard.png&quot; data-origin-width=&quot;1113&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9RYwK/btqCTwiecYS/5agBooh6PvRPo8xnPZKW7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9RYwK/btqCTwiecYS/5agBooh6PvRPo8xnPZKW7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9RYwK/btqCTwiecYS/5agBooh6PvRPo8xnPZKW7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9RYwK%2FbtqCTwiecYS%2F5agBooh6PvRPo8xnPZKW7K%2Fimg.png&quot; data-filename=&quot;standard.png&quot; data-origin-width=&quot;1113&quot; data-origin-height=&quot;154&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Intelligent-Tiering&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;사용자가 스토리지를 사용하는 패턴을 모니터링하고 비용 절감에 효과적인 스토리지 클래스로 자동 전환&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;분류 규칙에 따라 Standard / Standard-IA로 자동 스위칭&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;요금&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스토리지 요금&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cR6ODc/btqCXu4aEjt/0Z08r0VVyQBJNnTfGw4bd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cR6ODc/btqCXu4aEjt/0Z08r0VVyQBJNnTfGw4bd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cR6ODc/btqCXu4aEjt/0Z08r0VVyQBJNnTfGw4bd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcR6ODc%2FbtqCXu4aEjt%2F0Z08r0VVyQBJNnTfGw4bd0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다른 클래스와는 달리 &lt;strong&gt;모니터링 요금&lt;/strong&gt;이 추가된다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Standard-IA (Infrequent Access)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;자주 액세스하진 않지만 필요 시 빠르게 접근해야 하는 유형으로, 30일 이상 액세스가 잘 이루어지지 않는다면 고려해볼만한 클래스&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;이슈성 데이터에도 적합 (업로드 시 잦은 액세스, 시간이 지나며 트래픽 감소, 특정 시기에 트래픽 다시 급등하는 경우)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;요금&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스토리지 요금&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccmVNQ/btqCX8zLSU0/kK74XQ7UlTgbNpCn1YwByk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccmVNQ/btqCX8zLSU0/kK74XQ7UlTgbNpCn1YwByk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccmVNQ/btqCX8zLSU0/kK74XQ7UlTgbNpCn1YwByk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccmVNQ%2FbtqCX8zLSU0%2FkK74XQ7UlTgbNpCn1YwByk%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One Zone-IA&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;S3는 사용자가 모르게 3곳 이상의 물리적 영역에 백업이 되지만, One Zone은 1곳의 물리적 영역에만 데이터를 저장&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One Zone이라는 특성으로 인해 처리가 곤란하지만 약간 유실되어도 서비스에 큰 영향을 주지 않는 데이터 백업에 저장 (로그형 데이터)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;요금&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스토리지 요금&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y3E9v/btqCU7WnDds/7vNdoIq0KYj3BLM24kjHZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y3E9v/btqCU7WnDds/7vNdoIq0KYj3BLM24kjHZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y3E9v/btqCU7WnDds/7vNdoIq0KYj3BLM24kjHZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY3E9v%2FbtqCU7WnDds%2F7vNdoIq0KYj3BLM24kjHZK%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Glacier&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;가장 저렴한 스토리지 요금&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;가장 비싼 트래픽 요금&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;백업이 필요하지만 거의 접근하지 않는 데이터에 적합&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;요금&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;스토리지 요금 (Glacier/Glacier Deep Archive)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dg9ANS/btqCTxIfuEW/RPJ5KNHgugMvK1WGWLAU01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dg9ANS/btqCTxIfuEW/RPJ5KNHgugMvK1WGWLAU01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dg9ANS/btqCTxIfuEW/RPJ5KNHgugMvK1WGWLAU01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdg9ANS%2FbtqCTxIfuEW%2FRPJ5KNHgugMvK1WGWLAU01%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqOJWF/btqCX9yGNi9/nFhgtItvjFEmb4s7WdVDv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqOJWF/btqCX9yGNi9/nFhgtItvjFEmb4s7WdVDv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqOJWF/btqCX9yGNi9/nFhgtItvjFEmb4s7WdVDv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqOJWF%2FbtqCX9yGNi9%2FnFhgtItvjFEmb4s7WdVDv1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;각 클래스의 트래픽 요금&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddwp6a/btqCXwubCa8/IFwCVV5O59OH9kvSJNuod0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddwp6a/btqCXwubCa8/IFwCVV5O59OH9kvSJNuod0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddwp6a/btqCXwubCa8/IFwCVV5O59OH9kvSJNuod0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fddwp6a%2FbtqCXwubCa8%2FIFwCVV5O59OH9kvSJNuod0%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Standard의 트래픽 요금이 가장 저렴하다.&lt;/li&gt;
&lt;li&gt;스토리지 요금이 저렴할 수록 트래픽 요금은 비싸게 책정되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;S3 버킷 생성&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/ko/s3/&quot;&gt;https://aws.amazon.com/ko/s3/&lt;/a&gt; 에 접속하여 시작하기 버튼을 누르면 S3용 콘솔창이 나타난다.&lt;/p&gt;
&lt;p&gt;먼저 버킷을 생성해야 한다.&lt;/p&gt;
&lt;p&gt;리전과 이름만 변경하고 초기 옵션 그대로 생성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0UQ3X/btqCYFD7OfN/o73DWd2wmkS2oG3Hz5Gv4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0UQ3X/btqCYFD7OfN/o73DWd2wmkS2oG3Hz5Gv4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0UQ3X/btqCYFD7OfN/o73DWd2wmkS2oG3Hz5Gv4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0UQ3X%2FbtqCYFD7OfN%2Fo73DWd2wmkS2oG3Hz5Gv4K%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btspTk/btqCYFYqBqV/HGanxMDbEuSKkxkTRAblz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btspTk/btqCYFYqBqV/HGanxMDbEuSKkxkTRAblz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btspTk/btqCYFYqBqV/HGanxMDbEuSKkxkTRAblz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtspTk%2FbtqCYFYqBqV%2FHGanxMDbEuSKkxkTRAblz1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;객체 잠금 옵션은 모든 데이터를 읽기 전용으로 저장한다.&lt;ul&gt;
&lt;li&gt;보존 기간을 설정할 수 있다. 설정하지 않으면 영구적으로 보존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AWS SDK for Java&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/ko/developers/getting-started/java/&quot;&gt;Reference&lt;/a&gt; (AWS SDK 1.11)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Run Sample Code&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;샘플 프로젝트 다운로드&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; git clone https://github.com/awslabs/aws-java-sample.git&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;액세스 키 구성&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;http://aws.amazon.com/security-credentials&quot;&gt;http://aws.amazon.com/security-credentials&lt;/a&gt; 접속&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;액세스 키 탭 클릭 후 생성&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;키는 생성 즉시 다운로드 해서 보관하지 않으면 다시 찾을 수 없다. (아래 사진 참조)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2hVYX/btqCTxO2mU0/7cCZKQpI6Hy59RgKTkIrg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2hVYX/btqCTxO2mU0/7cCZKQpI6Hy59RgKTkIrg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2hVYX/btqCTxO2mU0/7cCZKQpI6Hy59RgKTkIrg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2hVYX%2FbtqCTxO2mU0%2F7cCZKQpI6Hy59RgKTkIrg1%2Fimg.png&quot; width=&quot;100%&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;액세스 키 파일 생성&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Windows&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C:\Users\{USER_NAME}\.aws\credentials&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Linux&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.aws/credentials&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;파일 내용 (value 값을 본인의 ID로 교체)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  [default]
  aws_access_key_id = YOUR_ACCESS_KEY_ID
  aws_secret_access_key = YOUR_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;샘플 프로젝트 컴파일/실행&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; mvn clean compile exec:java&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;p&gt;만약 메이븐 빌드 중 501 에러가 발생한다면 보통은 central repository의 프로토콜 정책 변경 때문이다. pom 파일에 아래의 내용을 추가한다. (&lt;a href=&quot;https://stackoverflow.com/questions/59763531/maven-dependencies-are-failing-with-a-501-error&quot;&gt;Reference&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;pluginRepositories&amp;gt;
      &amp;lt;pluginRepository&amp;gt;
          &amp;lt;id&amp;gt;central&amp;lt;/id&amp;gt;
          &amp;lt;name&amp;gt;Central Repository&amp;lt;/name&amp;gt;
          &amp;lt;url&amp;gt;https://repo.maven.apache.org/maven2&amp;lt;/url&amp;gt;
          &amp;lt;layout&amp;gt;default&amp;lt;/layout&amp;gt;
          &amp;lt;snapshots&amp;gt;
              &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
          &amp;lt;/snapshots&amp;gt;
          &amp;lt;releases&amp;gt;
              &amp;lt;updatePolicy&amp;gt;never&amp;lt;/updatePolicy&amp;gt;
          &amp;lt;/releases&amp;gt;
      &amp;lt;/pluginRepository&amp;gt;
  &amp;lt;/pluginRepositories&amp;gt;
  &amp;lt;repositories&amp;gt;
      &amp;lt;repository&amp;gt;
          &amp;lt;id&amp;gt;central&amp;lt;/id&amp;gt;
          &amp;lt;name&amp;gt;Central Repository&amp;lt;/name&amp;gt;
          &amp;lt;url&amp;gt;https://repo.maven.apache.org/maven2&amp;lt;/url&amp;gt;
          &amp;lt;layout&amp;gt;default&amp;lt;/layout&amp;gt;
          &amp;lt;snapshots&amp;gt;
              &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
          &amp;lt;/snapshots&amp;gt;
      &amp;lt;/repository&amp;gt;
  &amp;lt;/repositories&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;실행하면 임시 버킷 생성 - 버킷 목록 - 임의의 파일 업로드 - 파일 다운로드 - 파일 삭제 - 버킷 삭제 순으로 동작한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;실행이 완료되면 로그를 확인한다. 계정이 잘 연결되었다면 목록 중 {USER_BUCKET_NAME}에 내가 생성한 버킷의 이름이 나타난다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ===========================================
  Getting Started with Amazon S3
  ===========================================

  Creating bucket my-first-s3-bucket-bb3cc539-ade5-4dbb-b27f-190cd21bf0f4

  Listing buckets
      - my-first-s3-bucket-bb3cc539-ade5-4dbb-b27f-190cd21bf0f4
      - {USER_BUCKET_NAME}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Develop Example Code&lt;/h3&gt;
&lt;p&gt;Sample Code는 임시 버킷을 생성해서 API를 어떻게 활용하는지를 보여주었다.&lt;/p&gt;
&lt;p&gt;예제 코드를 바탕으로, 필요 위주로 재구성을 해보았다.&lt;/p&gt;
&lt;p&gt;참고로 예제는 AWS SDK 1.9.6 버전을 사용하고 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개발 환경&lt;ul&gt;
&lt;li&gt;AWS SDK 1.11&lt;/li&gt;
&lt;li&gt;JDK 1.7&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JDK 1.7을 사용한 이유는 AWS SDK를 적용해야 할 사내 서버가 1.7이기 때문이다.&lt;/p&gt;
&lt;p&gt;만약 &lt;strong&gt;AWS SDK 2.0+&lt;/strong&gt;을 사용할 경우 JDK도 1.8 이상을 사용해야 한다.&lt;/p&gt;
&lt;p&gt;DynamoDB와 같은 기능은 2.0 이상에서 아직 지원되지 않으니 목적에 맞는 SDK를 선택한다. (&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/sdk-for-java/v2/migration-guide/whats-different.html&quot;&gt;Reference&lt;/a&gt;)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Maven&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;예제에선 Region을 US-WEST-2로 설정하고 임시 버킷을 생성했다. 서울 리전에 생성한 내 버킷을 직접 연결한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS SDK 1.9.6에는 서울 리전 코드 &lt;code&gt;Regions.AP_NORTHEAST_2&lt;/code&gt; 가 포함되어 있지 않다. SDK를 최신 버전으로 교체했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;dependencies&amp;gt;
     &amp;lt;dependency&amp;gt;
         &amp;lt;groupId&amp;gt;com.amazonaws&amp;lt;/groupId&amp;gt;
         &amp;lt;artifactId&amp;gt;aws-java-sdk&amp;lt;/artifactId&amp;gt;
         &amp;lt;version&amp;gt;1.11.749&amp;lt;/version&amp;gt;
     &amp;lt;/dependency&amp;gt;
 &amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AmazonS3&lt;/code&gt; 인스턴스 생성&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AmazonS3Client&lt;/code&gt; 생성자는 최신 버전에 Deprecated 되었다. 빌더를 사용하여 인스턴스를 할당했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String accessKey = &amp;quot;&amp;quot;;
String secretKey = &amp;quot;&amp;quot;;

AmazonS3 s3 = AmazonS3ClientBuilder.standard()
    .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
    .withRegion(Regions.AP_NORTHEAST_2)
    .build();&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;withCredentials로 인증을 시도한다. 액세스 키를 이 곳에 등록하여 사용한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;withRegion로 가져오려는 버킷의 리전을 설정한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;버킷 목록 가져오기&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;예제 코드와 달라진 것이 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void listingBucket(AmazonS3 s3) {
    System.out.println(&amp;quot;Listing buckets&amp;quot;);
    for (Bucket bucket : s3.listBuckets()) {
        System.out.println(bucket.getName());
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;파일 업로드&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;시험용 이미지로 많이 쓰이는 Lenna.png를 업로드한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;resources에 &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%A0%88%EB%82%98_(%EC%9D%B4%EB%AF%B8%EC%A7%80)#/media/%ED%8C%8C%EC%9D%BC:Lenna.png&quot;&gt;Lenna.png&lt;/a&gt;를 포함시켰다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;objectKey는 버킷 내 유일하게 존재해야 하는 &lt;strong&gt;파일 식별자&lt;/strong&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void uploadLennaToS3(AmazonS3 s3) throws Exception {
    String objectKey = &amp;quot;lenna-&amp;quot; + UUID.randomUUID();
    System.out.println(&amp;quot;Uploading a new Lenna to S3&amp;quot;);
    s3.putObject(new PutObjectRequest(bucketName, objectKey, createLennaPngFile()));
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;특정 폴더에 업로드하는 경우&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;key에 폴더명을 prefix로 추가한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;myFolder 아래에 Lenna.png를 업로드하였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void uploadLennaToS3Folder(AmazonS3 s3) throws Exception {
    String objectKey = &amp;quot;lenna-&amp;quot; + UUID.randomUUID();
    System.out.println(&amp;quot;Uploading a new Lenna to S3&amp;quot;);
    s3.putObject(new PutObjectRequest(bucketName, &amp;quot;myFolder/&amp;quot; + objectKey, createLennaPngFile()));
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;파일 교체 (업데이트)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;이미 존재하는 key에 새 파일을 넣으면 파일이 새 파일로 교체된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void updateImage(AmazonS3 s3, String objectKey) throws Exception {
    System.out.println(&amp;quot;Updating the lenna to other image.&amp;quot;);
    s3.putObject(new PutObjectRequest(bucketName, objectKey, createS3LogoImageFile()));
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;파일 목록&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;prefix로 폴더명을 주면 특정 폴더의 파일을 전부 호출할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;prefix는 파일명을 사용할 수도 있고 폴더명을 사용해도 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void listing(AmazonS3 s3, String prefix) {
    System.out.println(&amp;quot;Listing objects (&amp;quot; + prefix + &amp;quot;)&amp;quot;);
    ObjectListing objectListing = s3.listObjects(new ListObjectsRequest()
          .withBucketName(bucketName)
          .withPrefix(prefix));

    for (S3ObjectSummary summary : objectListing.getObjectSummaries()) {
        System.out.println(&amp;quot;key: &amp;quot; + summary.getKey() + &amp;quot;, size: &amp;quot; + summary.getSize());
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;파일 다운로드&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;다운로드 API를 수행하면 객체의 메타데이터와 콘텐츠를 읽기 위한 Input Stream을 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void download(AmazonS3 s3, String objectKey) {
    System.out.println(&amp;quot;Downloading an object&amp;quot;);
    S3Object object = s3.getObject(new GetObjectRequest(bucketName, objectKey));
    System.out.println(&amp;quot;Content-Type of Lenna: &amp;quot;  + object.getObjectMetadata().getContentType());
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예제 코드는 github에 업로드되어 있다. (&lt;a href=&quot;https://github.com/dhmin5693/aws-s3-api-sample.git&quot;&gt;Repository Link&lt;/a&gt;)&lt;/p&gt;</description>
      <category>Infra &amp;amp; Dev tools/Cloud platform</category>
      <category>API</category>
      <category>AWS</category>
      <category>S3</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/93</guid>
      <comments>https://private-space.tistory.com/93#entry93comment</comments>
      <pubDate>Tue, 24 Mar 2020 22:41:48 +0900</pubDate>
    </item>
    <item>
      <title>DDD 아키텍처</title>
      <link>https://private-space.tistory.com/92</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788993827446&amp;amp;orderClick=LAG&amp;amp;Kc=&quot;&gt;최범균 님의 DDD Start&lt;/a&gt;를 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;DDD 아키텍처&lt;/h1&gt;
&lt;h2&gt;네 가지 영역&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처를 설계할 때 등장하는 4가지의 전형적인 영역이 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;표현
&lt;ul&gt;
&lt;li&gt;사용자의 요청을 응용 영역에 전달&lt;/li&gt;
&lt;li&gt;응용 영역의 처리 결과를 (시각화하여) 사용자에게 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;응용
&lt;ul&gt;
&lt;li&gt;사용자에게 제공해야 할 기능 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;도메인
&lt;ul&gt;
&lt;li&gt;도메인 모델을 로직으로 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인프라 스트럭처
&lt;ul&gt;
&lt;li&gt;DBMS, Message Queue 등 인프라 영역과의 연계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에 빗대면 아래 그림과 같은 구조를 이룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh38Ov/btqCO1UUNn7/jo5z4EIoDe0riK4EG5uFMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh38Ov/btqCO1UUNn7/jo5z4EIoDe0riK4EG5uFMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh38Ov/btqCO1UUNn7/jo5z4EIoDe0riK4EG5uFMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh38Ov%2FbtqCO1UUNn7%2Fjo5z4EIoDe0riK4EG5uFMK%2Fimg.png&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;506&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현 계층은 응용 계층에 의존적이고, 응용은 도메인에, 도메인은 인프라 스트럭처에 의존한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따라 상위 계층이 하위 계층에 의존하지 않을 수 있지만, 하위 계층은 상위 계층을 의존하지는 않는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시 코드는 금액 계산 로직이 복잡하여 &lt;b&gt;'인프라 스트럭처'&lt;/b&gt; 계층에 속한 룰 엔진을 적용한 것이라고 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Drools 룰 엔진의 evaluate 메소드는 연산을 수행하는 코드라고만 이해하고 예시를 보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DroolsRuleEngine&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt; public class DroolsRuleEngine {
     ...

     public void evaluate(String sessionName, List&amp;lt;?&amp;gt; facts) {
         // evaluate value
     }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CalculateDiscountService&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt; public class CalculateDiscountService {
     private DroolsRuleEngine ruleEngine;

     public CalculateDiscountService() {
         ruleEngine = new ruleEngine();
     }

     public Money calculateDiscount(List&amp;lt;OrderLine&amp;gt; orderLines, String customerId) {
         Customer customer = findCustomer(customerId);
         MutableMoney money = new MutableMoney(0);
         List&amp;lt;?&amp;gt; facts = Arrays.asList(customer, money);
         facts.addAll(orderLines);
         ruleEngine.evaluate(&quot;discountCalculation&quot;, facts);
         return money.toImmutableMoney();
     }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 당연히 잘 동작하겠지만, 몇 가지 문제점이 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CalculateDiscountService&lt;/code&gt;를 단독으로 테스트하기 어렵다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DroolsRuleEngine&lt;/code&gt;이 잘 작동함을 보장해야 &lt;code&gt;CalculateDiscountService&lt;/code&gt;의 동작도 보장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;구현 방식을 변경하기 어렵다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;만약 룰 엔진을 변경한다고 가정한다.&lt;/li&gt;
&lt;li&gt;calculateDiscount 메소드를 변경해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;CalculateDiscountService&lt;/code&gt;가 &lt;code&gt;DroolsRuleEngine&lt;/code&gt;에 강하게 결합되어 있기 때문에 생기는 문제들이다.&lt;/p&gt;
&lt;h2&gt;의존 역전 원칙 (Dependency Inversion Principle)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고수준(상위 계층) 모듈을 사용하기 위해선 저수준(하위 계층)이 제대로 동작해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 된다면 위 예제의 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위한 전략이 바로 DIP이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저수준 모듈이 고수준 모듈에 의존하도록 바꾸는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제의 코드를 약간 수정해보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RuleDiscounter&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt; public interface RuleDiscounter {
     public Money applyRules(Customer customer, List&amp;lt;OrderLine&amp;gt; orderLines);
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CalculateDiscountService&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt; public class CalculateDiscountService {
     private RuleDiscounter ruleDiscounter;

     public CalculateDiscountService(RuleDiscounter ruleDiscounter) {
         this.ruleDiscounter = ruleDiscounter;
     }

     public Money calculateDiscount(List&amp;lt;OrderLine&amp;gt; orderLines, String customerId) {
         Customer customer = findCustomer(customerId);
         return ruleDiscounter.applyRules(customer, orderLines);
     }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DroolsRuleEngine&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt; public class DroolsRuleEngine implements RuleDiscounter {
     ...

     @Override
     public Money applyRules(Customer customer, List&amp;lt;OrderLine&amp;gt; orderLines) {
         // Do something...
     }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스를 활용하여 의존 구조를 완전히 바꾸었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더불어 calculateDiscount 메소드에서 처리하던 로직을 ruleDiscounter에 분배하여 코드도 조금 더 깔끔해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;CalculateDiscountService&lt;/code&gt; 를 테스트할 때 &lt;code&gt;RuleDiscounter&lt;/code&gt; 에 적당한 Mock Object를 주입하면 된다.&lt;/p&gt;
&lt;p&gt;구현을 바꾸고 싶다면 &lt;code&gt;DroolsRuleEngine&lt;/code&gt; 대신 다른 구현체를 사용하면 된다.&lt;/p&gt;
&lt;p&gt;제시했던 2가지 문제는 DIP로 해결하였다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;고수준 - 의미 있는 기능을 제공하는 모듈&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;저수준 - 고수준의 기능을 구현하는 하위 기능의 실제 구현&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CalculateDiscountService&lt;/code&gt; 은 할인 금액을 계산하는 응용 계층인 고수준 모듈이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RuleDiscounter&lt;/code&gt; 는 '룰을 이용한 할인 금액 계산'이라는 의미와 기능을 지닌 고수준 모듈이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DroolsRuleEngine&lt;/code&gt; 은 고수준 모듈의 하위 기능인 &lt;code&gt;RuleDiscounter&lt;/code&gt; 의 구현체이므로 저수준 모듈이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다이어그램으로 그려보면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kz5IK/btqCMm0iTN3/bSkOdtIJcGGIeEAyhv4om1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kz5IK/btqCMm0iTN3/bSkOdtIJcGGIeEAyhv4om1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kz5IK/btqCMm0iTN3/bSkOdtIJcGGIeEAyhv4om1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkz5IK%2FbtqCMm0iTN3%2FbSkOdtIJcGGIeEAyhv4om1%2Fimg.png&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;423&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저수준 모듈이 고수준 모듈에 의존하는 형태가 되며 의존 관계가 역전되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DIP는 객체 지향 5대 원칙 SOLID에도 속하는 아주 중요한 개념이므로 잘 알아둘 필요가 있다.&lt;/p&gt;
&lt;h3&gt;DIP 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DIP의 핵심은 저수준 모듈이 고수준 모듈에 의존하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 잘못된 구조를 설계한다면 DIP가 아니라 일반적인 의존 계층을 지닐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bghHQ9/btqCMn54Be3/Zb3tZrE1fO5a13Qh0zi57K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bghHQ9/btqCMn54Be3/Zb3tZrE1fO5a13Qh0zi57K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bghHQ9/btqCMn54Be3/Zb3tZrE1fO5a13Qh0zi57K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbghHQ9%2FbtqCMn54Be3%2FZb3tZrE1fO5a13Qh0zi57K%2Fimg.png&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;401&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;인터페이스가 &lt;code&gt;RuleDiscounter&lt;/code&gt; 에서 &lt;code&gt;RuleEngine&lt;/code&gt; 으로 바뀌었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RuleEngine&lt;/code&gt; 의 개념은 사용할 룰 엔진을 정의하는 것이므로 저수준 모듈에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 DIP가 무너지며 맨 처음에 작성했던 코드가 갖는 문제를 똑같이 갖게 된다.&lt;/p&gt;
&lt;h3&gt;DIP 위반을 고려해도 되는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DIP는 코드에 내재된 문제를 해결해주는 좋은 전략이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 DIP가 항상 정답인 것은 아니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt; 은 Spring에서 트랜잭션 처리를 도와주는 어노테이션으로, 단 한 줄 써놓기만 하면 복잡한 설정을 일정 영역 내에 전부 적용할 수 있다.&lt;/p&gt;
&lt;p&gt;그러나, &lt;code&gt;@Transaction&lt;/code&gt;의 사용은 Spring에 대한 의존도가 굉장히 높아질 수밖에 없다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 선택지를 고려해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;DIP를 위해 설정하는 과정을 직접 구현한다. 단, 구현 과정은 매우 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇이 나은지는 개발자가 선택할 일이지만, 대부분의 경우 2가 더 낫다.&lt;/p&gt;
&lt;h2&gt;도메인 영역의 구성요소&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Entity
&lt;ul&gt;
&lt;li&gt;고유 식별자는 갖는 객체로, 도메인의 고유한 개념 표현&lt;/li&gt;
&lt;li&gt;도메인 모델의 데이터와 관련 기능 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Value
&lt;ul&gt;
&lt;li&gt;식별자가 없는 객체&lt;/li&gt;
&lt;li&gt;개념적으로 속성을 표현할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Aggregate
&lt;ul&gt;
&lt;li&gt;관련된 entity와 value의 묶음&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://private-space.tistory.com/89&quot;&gt;이전 포스트&lt;/a&gt;의 예제에서 &lt;code&gt;Order&lt;/code&gt; , &lt;code&gt;OrderLine&lt;/code&gt; 등 주문과 관련된 요소는 '주문' 애그리거트로 묶을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Repository
&lt;ul&gt;
&lt;li&gt;도메인 모델의 영속성 처리 (DB 등 활용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Domain Service
&lt;ul&gt;
&lt;li&gt;특정 엔티티에 속하지 않는 도메인 로직 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;애그리거트 (Aggregate)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 크기가 커질수록 도메인은 복잡해질 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 땐 개별적인 요소보다 큰 군집인 애그리거트의 관점에서 바라보는 것이 전체 구조를 이해하는데 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BwhFA/btqCKhev1SK/LUKdfS9N1vLnZt7WaHFtpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BwhFA/btqCKhev1SK/LUKdfS9N1vLnZt7WaHFtpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BwhFA/btqCKhev1SK/LUKdfS9N1vLnZt7WaHFtpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBwhFA%2FbtqCKhev1SK%2FLUKdfS9N1vLnZt7WaHFtpK%2Fimg.png&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;441&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문의 예시를 다시 한번 살펴보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;도메인 개념'주문'
&lt;ol&gt;
&lt;li&gt;주문&lt;/li&gt;
&lt;li&gt;배송지 정보&lt;/li&gt;
&lt;li&gt;주문자&lt;/li&gt;
&lt;li&gt;주문 목록&lt;/li&gt;
&lt;li&gt;총 결제 금액&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'주문'은 하위 5개 도메인 및 밸류를 묶어 표현하기에 적당하다고 여겨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애그리거트에 진입하기 위해선 적절한 루트 엔티티를 선정할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'주문'은 하위 개념인 배송지 정보, 주문자 등을 입력받아 &lt;b&gt;루트 엔티티&lt;/b&gt;로 사용하기 적절하며, 모든 정보를 갖는 완전한 객체로 사용해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;주문 정보에서 배송지 정보나 주문 목록, 총 결제 금액이 빠질 수는 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;레포지토리 (Repository)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 객체를 계속 사용하려면 DB와 같은 외부 저장소를 활용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 저장소를 활용하는 도메인 모델을 레포지토리라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리는 인프라 스트럭처 영역에 속하며 애그리거트 단위로 CRUD를 수행한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;주문 애그리거트를 위한 레포지토리&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;  public interface OrderRepository {
      Order findByNumber(OrderNumber number);
      void save(Order order);
      void delete(Order order);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java/Domain Driven Design</category>
      <category>DDD</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/92</guid>
      <comments>https://private-space.tistory.com/92#entry92comment</comments>
      <pubDate>Wed, 18 Mar 2020 01:48:59 +0900</pubDate>
    </item>
    <item>
      <title>About me</title>
      <link>https://private-space.tistory.com/notice/91</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Github&lt;/b&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1604854753412&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;dhmin5693 - Overview&quot; data-og-description=&quot;BE Junior. dhmin5693 has 17 repositories available. Follow their code on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/dhmin5693&quot; data-og-url=&quot;https://github.com/dhmin5693&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/7KOyh/hyIaBo4kG5/urixOikA9E5tnk1QSK95Vk/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420,https://scrap.kakaocdn.net/dn/TNfU6/hyIaBiiLii/MwxuKSuZSpZ7JknnEz4Tkk/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420&quot;&gt;&lt;a href=&quot;https://github.com/dhmin5693&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/dhmin5693&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/7KOyh/hyIaBo4kG5/urixOikA9E5tnk1QSK95Vk/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420,https://scrap.kakaocdn.net/dn/TNfU6/hyIaBiiLii/MwxuKSuZSpZ7JknnEz4Tkk/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;dhmin5693 - Overview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;BE Junior. dhmin5693 has 17 repositories available. Follow their code on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;E-mail&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;dhmin5693@naver.com&lt;/p&gt;</description>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/notice/91</guid>
      <pubDate>Tue, 17 Mar 2020 17:20:47 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot 자동 설정 분석</title>
      <link>https://private-space.tistory.com/90</link>
      <description>&lt;h1&gt;Spring boot 자동 설정&lt;/h1&gt;
&lt;p&gt;Spring boot로 와서 가장 크게 달라진 점은 설정 부분이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@SpringBootApplication&lt;/code&gt; 하나만 추가하면 필요한 모든 설정을 알아서 추가한다.&lt;/p&gt;
&lt;p&gt;이 자동 설정 기능만을 위해 &lt;strong&gt;autoconfigure&lt;/strong&gt;라는 모듈이 작성되어 있고 그 덕분에 Spring boot의 핵심인 &lt;code&gt;@SpringBootApplication&lt;/code&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;가시적으로 새로운 기능은 아니지만 편리성을 비약적으로 상승시켜 생산성을 어마어마하게 높여주었다.&lt;/p&gt;
&lt;h2&gt;@SpringBootApplication&lt;/h2&gt;
&lt;p&gt;Spring boot 프로젝트를 생성하면 main 메소드가 포함된 클래스에 자동으로 추가되는 어노테이션이다.&lt;/p&gt;
&lt;p&gt;어노테이션의 정의를 타고 올라가면 &lt;code&gt;SpringBootApplication&lt;/code&gt;의 코드를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM,
                classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

    @AliasFor(annotation = EnableAutoConfiguration.class)
    Class&amp;lt;?&amp;gt;[] exclude() default {};

    @AliasFor(annotation = EnableAutoConfiguration.class)
    String[] excludeName() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = &amp;quot;basePackages&amp;quot;)
    String[] scanBasePackages() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = &amp;quot;basePackageClasses&amp;quot;)
    Class&amp;lt;?&amp;gt;[] scanBasePackageClasses() default {};
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;7가지 어노테이션이 달려있지만 스프링 고유 어노테이션은 아래 3개 뿐이다.&lt;/p&gt;
&lt;p&gt;하나씩 따라가본다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;code&gt;@SpringBootConfiguration&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration { }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@Configuration&lt;/code&gt; 하나를 제외하면 모두 자바 기본 어노테이션이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Configuration&lt;/code&gt; 어노테이션이 붙어 있으니 메인 클래스도 Bean으로 등록된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component
@RequiredArgsConstructor
public class AppRunner implements ApplicationRunner {

    // main 메소드를 포함하는 클래스
    private final DemoApplication demoApplication;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(demoApplication);
        // com.example.demo.DemoApplication$$EnhancerBySpringCGLIB$$17e91336@236ab296 출력
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;code&gt;@EnableAutoConfiguration&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;이 어노테이션에는 2가지 하위 어노테이션이 포함된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@AutoConfigurationPackage&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@Import(AutoConfigurationImportSelector.class)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;@AutoConfigurationPackage&lt;/code&gt; 먼저 살펴본다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage { }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AutoConfigurationPackages.Registrar.class&lt;/code&gt;의 정의로 이동한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        register(registry, new PackageImport(metadata).getPackageName());
    }
    @Override
    public Set&amp;lt;Object&amp;gt; determineImports(AnnotationMetadata metadata) {
        return Collections.singleton(new PackageImport(metadata));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Registrar&lt;/code&gt;는 &lt;code&gt;ImportBeanDefinitionRegistrar&lt;/code&gt;의 구현체이다.&lt;/p&gt;
&lt;p&gt;bean 자동 등록 과정에서는 registerBeanDefinitions 메소드를 사용하고 있으며, 두 개의 매개변수를 받는다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AnnotationMetadata&lt;/code&gt; metadata&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어노테이션이 부착된 클래스의 이름, 어노테이션의 데이터 등을 담고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;BeanDefinitionRegistry&lt;/code&gt; registry&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bean 라이프사이클에서 생성하는 beanFactory를 &lt;code&gt;BeanDefinitionRegistry&lt;/code&gt; 타입으로 변경한 것이다.&lt;/li&gt;
&lt;li&gt;생성부 : &lt;code&gt;BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;registerBeanDefinitions 메소드의 매개변수에 있는 &lt;code&gt;BeanDefinitionRegistry&lt;/code&gt; 타입의 인터페이스는 bean 정의를 관리하는 역할을 맡고 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BeanDefinitionRegistry&lt;/code&gt; 에 관한 내용은 &lt;a href=&quot;https://private-space.tistory.com/71&quot;&gt;이전에 작성한 포스트(bean 생성)&lt;/a&gt;에서 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;register(...)에 브레이크 포인트를 찍고 다음으로 몇 번 넘어가면 &lt;code&gt;ConfigurationClassBeanDefinitionReader&lt;/code&gt; 클래스의 loadBeanDefinitions 메소드에서 루프를 돌고 있는 모습이 보인다.&lt;/p&gt;
&lt;p&gt;forEach 내부의 &lt;code&gt;ConfigurationClass&lt;/code&gt; configClass가 autoconfigure에 등록된 자동 설정 정보를 갖고 있으며, 하나씩 돌면서 등록하는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;code&gt;@Import(AutoConfigurationImportSelector.class)&lt;/code&gt; 를 확인해 볼 차례다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AutoConfigurationImportSelector&lt;/code&gt;의 정의를 타고 올라가보자.&lt;/p&gt;
&lt;p&gt;내려가다 보면 getCandidateConfigurations라는 메소드가 있다.&lt;/p&gt;
&lt;p&gt;이 메소드는 자동 설정을 하고자 하는 후보군 클래스의 이름 목록을 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;protected List&amp;lt;String&amp;gt; getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List&amp;lt;String&amp;gt; configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                getBeanClassLoader());
        Assert.notEmpty(configurations, &amp;quot;No auto configuration classes found in META-INF/spring.factories. If you &amp;quot;
                + &amp;quot;are using a custom packaging, make sure that file is correct.&amp;quot;);
        return configurations;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Assert의 메시지를 보면 &lt;strong&gt;META-INF/spring.factories&lt;/strong&gt;에 클래스 정보가 있어야 함을 알 수 있다.&lt;/p&gt;
&lt;p&gt;spring.factories의 위치는 spring boot의 autoconfigure 모듈 아래에 존재한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-filename=&quot;캡처.PNG&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;187&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doqbJ4/btqCM96HqgI/O96Xs4rNC85CZIHkmT4Hg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doqbJ4/btqCM96HqgI/O96Xs4rNC85CZIHkmT4Hg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doqbJ4/btqCM96HqgI/O96Xs4rNC85CZIHkmT4Hg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoqbJ4%2FbtqCM96HqgI%2FO96Xs4rNC85CZIHkmT4Hg1%2Fimg.png&quot; data-filename=&quot;캡처.PNG&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;187&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이 파일을 열어보면 &lt;strong&gt;# Auto Configure&lt;/strong&gt; 주석 아래에 자동으로 컨픽을 등록할 클래스 목록이 나열되어 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
... # 이하 생략&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;code&gt;@ComponentScan&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@ComponentScan&lt;/code&gt;은 스프링을 공부해보았다면 이미 잘 아는 어노테이션이다.&lt;/p&gt;
&lt;p&gt;특정 위치의 &lt;code&gt;@Component&lt;/code&gt;를 전부 읽어 bean으로 등록하는 역할을 담당한다.&lt;/p&gt;
&lt;p&gt;단, excludeFilters 규칙이 걸려 있기 때문에 spring boot에 의해 자동 등록된 bean은 제외한다.&lt;/p&gt;
&lt;p&gt;이 어노테이션의 존재로 인해 메인 클래스의 위치가 굉장히 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;┌─ root
└─┬─ com
  └─┬─ example
    └─┬─ demo
      ├── controler
      ├── service
      ├── dto
      └── DemoApplication.java&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보통 이 경우 demo 아래에 DemoApplication.java가 메인 클래스로 존재하는데,&lt;br&gt;demo 패키지 이하는 사실상 프로젝트 전체를 포함한다.&lt;br&gt;만약 메인 클래스가 Dto 클래스에 위치한다면 controller나 service 패키지의 bean은 읽지 못한다.&lt;/p&gt;</description>
      <category>Java/Spring framework</category>
      <category>Spring Boot</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/90</guid>
      <comments>https://private-space.tistory.com/90#entry90comment</comments>
      <pubDate>Sun, 15 Mar 2020 15:42:27 +0900</pubDate>
    </item>
    <item>
      <title>간단한 예제로 DDD 입문하기 (Spring boot)</title>
      <link>https://private-space.tistory.com/89</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788993827446&amp;amp;orderClick=LAG&amp;amp;Kc=&quot;&gt;최범균 님의 DDD Start&lt;/a&gt;를 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;도메인&lt;/h1&gt;
&lt;h2&gt;도메인 모델&lt;/h2&gt;
&lt;p&gt;이커머스에서 모니터를 구매한다고 가정해보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;원하는 스펙의 모니터를 &lt;code&gt;검색&lt;/code&gt;하고 다양한 물건을 비교한다.&lt;/li&gt;
&lt;li&gt;눈에 띄는 물건이 여러 개라면 &lt;code&gt;장바구니&lt;/code&gt;에 넣어둘 수도 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;구매&lt;/code&gt;를 결정했다면 어떤 결제수단을 사용할지, 배송지는 어딘지를 선택한다.&lt;/li&gt;
&lt;li&gt;배송을 시작했다면 &lt;code&gt;배송 추적&lt;/code&gt; 기능을 활용하여 내 물건이 어디쯤 왔는지를 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;개발자에겐 &lt;code&gt;쇼핑몰&lt;/code&gt;은 구현 대상이다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;쇼핑몰&lt;/code&gt;에서는 구매를 위한 다양한 기능을 제공한다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;쇼핑몰&lt;/code&gt;은 소프트웨어로 해결하기 위한 &lt;strong&gt;도메인&lt;/strong&gt;에 해당한다.&lt;/p&gt;
&lt;h2&gt;정의&lt;/h2&gt;
&lt;p&gt;도메인 모델은 &lt;strong&gt;특정 도메인을 개념적으로 표현&lt;/strong&gt;한 것이다.&lt;/p&gt;
&lt;p&gt;비즈니스 로직에 맞는 규칙을 구현한 레이어는 도메인이 되어야 한다.&lt;/p&gt;
&lt;h2&gt;예시&lt;/h2&gt;
&lt;p&gt;상품 주문을 예시로 코드를 작성한다.&lt;/p&gt;
&lt;p&gt;코드 작성보다 선행되어야 할 것은 모델링이다.&lt;/p&gt;
&lt;h3&gt;모델의 규칙 정의&lt;/h3&gt;
&lt;p&gt;비즈니스 요구사항에 맞춰 규칙을 정의해보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 종류 이상의 상품을 주문해야 한다.&lt;/li&gt;
&lt;li&gt;한 상품을 한 개 이상 주문할 수 있다.&lt;/li&gt;
&lt;li&gt;총 주문 금액은 각 상품의 구매가 합을 의미한다.&lt;/li&gt;
&lt;li&gt;각 상품의 구매가격 합은 상품 가격 * 구매 수량을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;규칙이 다 정의되었으면 상태 다이어그램도 그려본다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;601&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2sbAb/btqCB8gCSaj/jTVvdxEBdDDGtG5RcpmdoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2sbAb/btqCB8gCSaj/jTVvdxEBdDDGtG5RcpmdoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2sbAb/btqCB8gCSaj/jTVvdxEBdDDGtG5RcpmdoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2sbAb%2FbtqCB8gCSaj%2FjTVvdxEBdDDGtG5RcpmdoK%2Fimg.png&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;601&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;코드 작성&lt;/h3&gt;
&lt;p&gt;위 규칙과 다이어그램을 바탕으로 주문 정보를 가질 객체 &lt;code&gt;Order&lt;/code&gt;를 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Order {

    private OrderNo id;
    private List&amp;lt;OrderLine&amp;gt; orderLines;
    private OrderState state;
    private ShippingInfo shippingInfo;
    private Money totalAmounts;

    public void payment() { ... }
    public void shipped() { ... }
    public void startDelivery() { ... }
    public void completeDelivery() { ... }
    public void cancel() { ... }

    public void changeShippingInfo(ShippingInfo newShippingInfo) { ... }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;Order&lt;/code&gt;에는 결제, 출고, 배송시작, 배송완료, 취소, 배송 정보 변경 기능이 포함되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OrderLine&lt;/code&gt;은 주문 정보를 의미한다. 상품 종류, 가격, 개수 등이 포함된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OrderState&lt;/code&gt;는 주문의 상태 정보이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ShippingInfo&lt;/code&gt;는 배송지 정보를 나타낸다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Money&lt;/code&gt;는 금액을 나타내는 &lt;strong&gt;밸류 타입&lt;/strong&gt;의 객체로 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;밸류 타입은 개념적으로 하나인 무언가를 표현하기 위해 사용한다.&lt;/li&gt;
&lt;li&gt;Money 대신 int를 사용해도 된다. Money는 int보다 더 명시적인 표현으로 개발자의 이해를 돕는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다른 주요 객체도 살펴보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;OrderLine&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public class OrderLine {
     private Product product;
     private Money price;
     private int quantity;
     private Money amounts;
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OrderState&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public enum OrderState {
     PAYMENT_WAITING {
         public boolean isShippingChangeable() {
             return true;
         }
     },

     PREPARING {
         public boolean isShippingChangeable() {
             return true;
         }
     },

     SHIPPED,
     DELIVERING,
     DELIVERING_COMPLETED,
     CANCELED;

     public boolean isShippingChangeable() {
         return false;
     }
 }&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;isShippingChangeable 메소드로 배송지 변경 가능 여부를 판별한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ShippingInfo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public class ShippingInfo {
     private Receiver receiver;
     private Address address;
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;코드를 전부 작성하면 테스트도 작성해보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;주문 실패&lt;ol&gt;
&lt;li&gt;받는 사람 정보가 없는 경우&lt;/li&gt;
&lt;li&gt;배송지 정보가 없는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;주문 성공&lt;ol&gt;
&lt;li&gt;각 단계를 무사히 넘어가는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;주문 취소&lt;ol&gt;
&lt;li&gt;지정된 단계에서만 취소가 가능한지?&lt;/li&gt;
&lt;li&gt;취소가 올바르게 되었는지?&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;배송지 정보 변경&lt;ol&gt;
&lt;li&gt;지정된 단계에서만 변경이 가능한지?&lt;/li&gt;
&lt;li&gt;변경된 배송지가 올바르게 적용되었는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;주의점&lt;/h3&gt;
&lt;p&gt;DDD 패턴으로 작성할 때는 몇 가지 신경써야 할 것이 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;도메인 객체는 get/set 메소드를 넣지 않는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;get/set은 도메인의 핵심 개념이나 의도를 표현하지 못한다.&lt;/li&gt;
&lt;li&gt;도메인은 해당 개념이 어떤 행위를 하고, 어떻게 상태가 변하는 지에 집중한다.&lt;/li&gt;
&lt;li&gt;set/get으로 객체의 상태값을 계속 바꿔서 사용하는 행위는 &amp;#39;객체지향&amp;#39;보다 &amp;#39;절차지향&amp;#39;에 가깝다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;도메인 객체는 완전한 상태로 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;주문을 하기 위해 &lt;code&gt;OrderLine&lt;/code&gt; , &lt;code&gt;ShippingInfo&lt;/code&gt; 는 반드시 포함되어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Order&lt;/code&gt; 생성 후 정보를 추가한다면 정보의 일부가 누락되는 상황이 생길 수 있다.&lt;ul&gt;
&lt;li&gt;주문 정보가 없다면?&lt;/li&gt;
&lt;li&gt;배송지 정보가 없다면?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;도메인 용어를 명확히 정의한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상품이 준비되어서 배송 올 때까지의 단계를 Step 1 ~ Step N으로 정의했다고 생각해보자.&lt;/li&gt;
&lt;li&gt;가독성이 크게 떨어진다.&lt;/li&gt;
&lt;li&gt;OrderState.PAYMENT_WAITTING처럼 상태에 대해 명확하게 정의한 단어는 개발자가 소스 코드를 이해하는데 필요한 시간을 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;밸류 타입은 불변 객체로 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;값의 변화를 허용하면 잘못된 참조로 인한 오류가 발생할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Money price = new Money(50000);  
OrderLine line = new OrderLine(product, price, quantity);  
price.setValue(30000);
// line.getPrice().getValue()가 갖는 값은?&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;전체 코드는 Github에 업로드되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dhmin5693/ddd-example-01&quot;&gt;소스 코드 확인하기&lt;/a&gt;&lt;/p&gt;</description>
      <category>Java/Domain Driven Design</category>
      <category>DDD</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/89</guid>
      <comments>https://private-space.tistory.com/89#entry89comment</comments>
      <pubDate>Wed, 11 Mar 2020 00:41:14 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 연관관계 매핑 기초</title>
      <link>https://private-space.tistory.com/87</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;김영한 님의 자바 ORM 표준 JPA 프로그래밍을 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788960777330&amp;amp;orderClick=LEa&amp;amp;Kc=&quot;&gt;교보문고 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;연관관계 매핑 기초&lt;/h1&gt;
&lt;p&gt;객체는 참조를 사용하여 연관 관계를 맺고 테이블은 외래 키를 사용해서 연관 관계를 맺는다.&lt;/p&gt;
&lt;p&gt;관계가 있는 다른 데이터를 참조한다는 점에서 동일하지만, 참조와 외래 키는 완전히 다른 특징을 갖는다.&lt;/p&gt;
&lt;p&gt;연관관계 매핑을 이해하기 위한 두 가지 키워드가 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;방향 : 회원/팀이라는 관계를 생각해본다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단방향 : 회원 → 팀, 팀 → 회원 둘 중 한 쪽만 참조하는 관계&lt;/li&gt;
&lt;li&gt;양방향 : 회원 → 팀, 팀 → 회원 둘이 서로를 참조하는 관계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다중성&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1:1 (일대일)&lt;/li&gt;
&lt;li&gt;1:N (일대다 혹은 다대일)&lt;/li&gt;
&lt;li&gt;N:M (다대다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;연관관계의 주인 : 양방향 연관관계를 만들 떄 연관 관계의 주인을 정해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;단방향 연관관계&lt;/h2&gt;
&lt;p&gt;가장 먼저 이해해야 할 것은 다대일 단방향 관계이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;다대일 연관관계.PNG&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgqFje/btqCimZ3WOw/fyozJxIhvrzYxnZK1RwIMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgqFje/btqCimZ3WOw/fyozJxIhvrzYxnZK1RwIMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgqFje/btqCimZ3WOw/fyozJxIhvrzYxnZK1RwIMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgqFje%2FbtqCimZ3WOw%2FfyozJxIhvrzYxnZK1RwIMk%2Fimg.png&quot; data-filename=&quot;다대일 연관관계.PNG&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;481&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;회원은 하나의 팀에만 소속될 수 있다.&lt;/li&gt;
&lt;li&gt;회원과 팀은 다대일 관계이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SQL로 회원에 대한 정보를 가져오고 싶을 때, 회원 중심 혹은 팀 중심의 쿼리를 입력할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;회원 중심&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; SELECT *
 FROM MEMBER M
 INNER JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;팀 중심&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; SELECT *
 FROM TEAM T
 INNER JOIN MEMBER M ON M.TEAM_ID = T.TEAM_ID&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;자바 객체는 필드를 사용하여 참조를 할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;회원 중심&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; class Team {
     Long id;
     ...
 }
 class Member {
     Long id;
     Team team;
     ...
 }

 // 참조는 아래처럼
 Member a = new Member();
 a.getTeam.getId();&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;팀 중심&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; class Team {
     Long id;
     Member id;
     ...
 }
 class Member {
     Long id;
 }

 // 참조는 아래처럼
 Team a = new Team();
 a.getMember.getId();&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;둘은 비슷해 보이지만 큰 차이가 있다.&lt;/p&gt;
&lt;p&gt;SQL문은 &lt;strong&gt;동일한 JOIN절&lt;/strong&gt; 하나로 양방향 조인이 가능했다.&lt;/p&gt;
&lt;p&gt;객체는 양방향 참조를 하기 위해서는 각 객체에 참조할 대상을 반드시 필드로 넣어줘야 한다.&lt;/p&gt;
&lt;h3&gt;객체 관계 매핑&lt;/h3&gt;
&lt;p&gt;JPA를 활용하여 연관관계를 매핑하려면 어노테이션을 사용한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Member class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 @Getter
 @Setter
 public class Member {

     @Id
     @Column (name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     @ManyToOne
     @JoinColumn (name = &amp;quot;TEAM_ID&amp;quot;)
     private Team team;
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Team class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 @Getter
 @Setter
 public class Team {

     @Id
     @Column (name = &amp;quot;TEAM_ID&amp;quot;)
     private Long id;

     private String name;
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;@ManyToOne&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다대일 관계를 나타내는 매핑 정보&lt;/li&gt;
&lt;li&gt;속성&lt;ul&gt;
&lt;li&gt;optional (default true) : false로 설정하면 연관된 엔티티가 반드시 있어야 함.&lt;/li&gt;
&lt;li&gt;fetch : 글로벌 패치 전략 설정&lt;/li&gt;
&lt;li&gt;cascade : 영속성 전이 기능 사용&lt;/li&gt;
&lt;li&gt;targetEntity : 연관된 엔티티의 타입 정보 설정 (targetEntity = Member.class 식으로 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;@JoinColumn (name=&amp;quot;TEAM_ID&amp;quot;)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외래 키 매핑 시 사용&lt;/li&gt;
&lt;li&gt;name 속성은 매핑할 외래키의 이름&lt;/li&gt;
&lt;li&gt;어노테이션을 생략해도 외래 키가 생성됨.&lt;ul&gt;
&lt;li&gt;생략 시 외래키의 이름이 기본 전략을 활용하여 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;속성&lt;ul&gt;
&lt;li&gt;name : 매핑할 외래 키의 이름&lt;/li&gt;
&lt;li&gt;referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명&lt;/li&gt;
&lt;li&gt;foreignKey : 외래 키 제약조건 지정 (테이블 생성 시에만 적용됨)&lt;/li&gt;
&lt;li&gt;unique/nullable/insertable/updateable/columnDefinition/table : &lt;code&gt;@Column&lt;/code&gt;의 속성과 같음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;연관관계 사용&lt;/h2&gt;
&lt;p&gt;CRUD 예제를 둘러보며 연관 관계 사용법을 익혀보자.&lt;/p&gt;
&lt;h3&gt;저장&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public void save() {

    Team team = Team.builder()
        .name(&amp;quot;team&amp;quot;);
        .build();

    em.persist(team);

    Member member1 = Member.builder()
        .username(&amp;quot;member1&amp;quot;)
        .team(team)
        .build();

    em.persist(member1);

    Member member2 = Member.builder()
        .username(&amp;quot;member2&amp;quot;);
        .team(team)
        .build();

    em.persist(member2);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;builder(), build()는 롬복 플러그인으로 생성되는 builder를 의미한다.&lt;/p&gt;
&lt;p&gt;위 예제에서 회원 엔티티는 팀 엔티티를 참조하고 저장했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;em.persist(team);&lt;/code&gt; 이 실행되는 순간 team 객체에는 id가 부여된다.&lt;/p&gt;
&lt;p&gt;내부적으로는 이를 활용한 쿼리가 생성되어 DB에 저장하게 된다.&lt;/p&gt;
&lt;h3&gt;조회&lt;/h3&gt;
&lt;p&gt;연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;객체 그래프 탐색 (객체 연관관계를 사용한 조회)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; Member member = em.find(Member.class, member1_id);
 Team team = member.getTeam();&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;객체지향 쿼리(JPQL) 사용&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; private statid void read(EntityManager em) {

     String jpql = &amp;quot;select m from Member m join m.team t where&amp;quot; +
         &amp;quot;t.name=:teamName&amp;quot;;

     List&amp;lt;Member&amp;gt; resultList = em.createQuery(jpql, Member.class)
         .setParameter(&amp;quot;teamName&amp;quot;, &amp;quot;team1&amp;quot;)
         .getResultList();
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;JPQL의 쿼리는 일반 쿼리처럼 ID 기반으로 조회하지 않고 엔티티 객체를 활용해서 조회하는 것에 가깝다.&lt;/p&gt;
&lt;p&gt;중간에 &lt;code&gt;t.name=:teamName&lt;/code&gt; 이라는 문구에서 :teamName과 같이 :로 시작하는 것은 파라미터를 바인딩받는 문법이다.&lt;/p&gt;
&lt;p&gt;위의 JPQL은 아래의 쿼리로 변환되어 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT M.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE T.NAME = &amp;#39;team1&amp;#39;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;수정&lt;/h3&gt;
&lt;p&gt;회원1의 소속을 팀1에서 팀2로 변경해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void update(EntityManager em) {

    Team team2 = Team.builder()
        .name(&amp;quot;team2&amp;quot;);
        .build();

    em.persist(team2);

    Member member = em.find(Member.class, member1_id);
    member.setTeam(team2);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;트랜잭션이 커밋되는 순간 플러시가 일어나고 변경 감지 기능이 동작한다.&lt;/p&gt;
&lt;p&gt;그리고 변경사항을 DB에 반영한다.&lt;/p&gt;
&lt;h3&gt;연관관계 제거&lt;/h3&gt;
&lt;p&gt;회원1의 소속을 제거하는 예제이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void delete(EntityManager em) {

    Member member1 = em.find(Member.class, member1_id);
    member1.setTeam(null);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;team을 null로 설정하면 연관된 엔티티가 삭제된다.&lt;/p&gt;
&lt;p&gt;만약 특정 팀을 삭제하고 싶다면 그 팀 소속의 회원 정보에서 특정 팀에 대한 정보를 모두 삭제해야 한다.&lt;/p&gt;
&lt;p&gt;(데이터베이스의 제약 조건을 따른다)&lt;/p&gt;
&lt;h2&gt;양방향 연관관계&lt;/h2&gt;
&lt;p&gt;지금까지 예제로는 회원이 어느 팀 소속이었는지 쉽게 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;이번엔 팀 소속의 회원이 누구인지를 더 쉽게 구해보는 양방향 연관 예제이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;회원은 1개의 소속 팀을 가질 수 있다. → Team (단일 객체 필드)&lt;/li&gt;
&lt;li&gt;팀에 소속된 회원은 N명이다 → List (복수 객체 필드)&lt;ul&gt;
&lt;li&gt;List 말고 다른 Collection들도 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Team 클래스는 List 필드를 갖지만 데이터베이스의 TEAM 테이블은 member id을 가질 필요가 없다.&lt;/p&gt;
&lt;p&gt;DB에서는 MEMBER 테이블의 TEAM_ID 외래키 하나로도 양방향을 구현할 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;양방향 관계를 갖도록 기존의 엔티티를 수정한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Member class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 @Getter
 @Setter
 public class Member {

     @Id
     @Column (name = &amp;quot;MEMBER_ID&amp;quot;)
     private Long id;

     private String username;

     @ManyToOne
     @JoinColumn (name = &amp;quot;TEAM_ID&amp;quot;)
     private Team team;
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Team class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 @Getter
 @Setter
 public class Team {

     @Id
     @Column (name = &amp;quot;TEAM_ID&amp;quot;)
     private Long id;

     private String name;

     @OneToMany (mappedBy = &amp;quot;team&amp;quot;)
     private List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;&amp;gt;();
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Team 클래스에 List 타입 필드를 추가했다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;@OneToMany&lt;/code&gt; 어노테이션으로 매핑 정보를 표시했다.&lt;/p&gt;
&lt;p&gt;mappedBy 속성은 양방향 매핑 시 반대쪽 매핑(Member 클래스의 Team team)값을 주면 된다.&lt;/p&gt;
&lt;p&gt;추가된 리스트로 인해 소속 회원을 쉽게 조회할 수 있게 되었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void memberList() {
    Team team = em.find(Team.class, team1_id);
    List&amp;lt;Member&amp;gt; members = team.getMembers();
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;연관관계의 주인&lt;/h2&gt;
&lt;p&gt;실제 쿼리는 TEAM_ID 하나로 양방향 매핑이 가능했지만, 객체 참조의 경우엔 양쪽 다 참조할 필드를 써주어야 한다.&lt;/p&gt;
&lt;p&gt;이런 경우, 두 테이블에 서로의 고유 키를 외래 키로 포함할 필요가 없다.&lt;/p&gt;
&lt;p&gt;mappedBy는 이런 상황에서 외래 키의 위치를 정의하기 위해 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;외래 키를 갖는 엔티티가 연관 관계의 주인&lt;/strong&gt;이 되며, mappedBy는 주인을 명시한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Team.members에는 mappedBy=&amp;quot;team&amp;quot; 속성을 사용한다.&lt;/li&gt;
&lt;li&gt;Member.team이 주인이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;양방향 연관관계 저장&lt;/h3&gt;
&lt;p&gt;mappedBy를 사용하여 양방향 연관관계를 설정 시 주의해야 할 점이 있다.&lt;/p&gt;
&lt;p&gt;주인이 아닌 필드는 &lt;strong&gt;변경하더라도 저장되지 않는다&lt;/strong&gt;는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void updateEntity(EntityManager em) {

    Team team = em.find(Team.class, team1_id);

    team.getMembers.add(
        Member.builder()
            .name(&amp;quot;member3&amp;quot;)
            .build();
    ); // 저장되지 않음
    team.getMembers.add(
        Member.builder()
            .name(&amp;quot;member4&amp;quot;)
            .build();
    ); // 저장되지 않음

    Member member1 = em.find(Member.class, member1_id);
    member1.setTeam(team2); // 저장됨
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 예시에서 members의 변동 내역은 저장되지 않는다.&lt;/p&gt;
&lt;p&gt;물리적인 TEAM 테이블에는 MEMBER 관련 컬럼이 존재하지 않기 때문이다.&lt;/p&gt;
&lt;p&gt;반대로 MEMBER 테이블에는 TEAM_ID라는 컬럼이 존재하기 때문에 밑 두 줄의 명령은 DB에 반영된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;회원의 소속 변경?&lt;/p&gt;
&lt;p&gt;만약 member1의 소속을 바꾸고 싶다면, Member 클래스와 Team 클래스를 모두 바꾸어주는 것이 좋다.&lt;/p&gt;
&lt;p&gt;테이블 관점에서는 Member 클래스의 team 필드만 바꿔주면 되지만 JPA로 불러왔으나 아직 값이 변하지 않은 순수 자바 객체인 경우 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  private void example(EntityManager em) {

      Team team2 = em.find(Team.class, team2_id);

      // member1의 소속 팀은 현재 1
      Member member1 = em.find(Member.class, member1_id);

      // member1의 소속 팀이 2로 변경됨
      member1.setTeam(team2);

      for (Member member : team2.getMembers()) {
          // member1이 team2의 회원 목록에 없어 출력되지 않는다.
          System.out.println(member.getUsername());
      }
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refactoring&lt;/p&gt;
&lt;p&gt;엔티티 코드를 조금만 변경하여 위와 같은 문제를 해결해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Entity
  @Getter
  @Setter
  public class Member {

      @Id
      @Column (name = &amp;quot;MEMBER_ID&amp;quot;)
      private Long id;

      private String username;

      @ManyToOne
      @JoinColumn (name = &amp;quot;TEAM_ID&amp;quot;)
      @Setter (AccessLevel.NONE)
      private Team team;

      public void setTeam(Team team) {

          if (this.team != null) {
              this.team.getMembers().remove(this);
          }

          this.team = team;
          team.getMembers().add(this);
      }
  }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;setTeam에서 소속 팀 변경 및 소속 팀의 회원 변경까지 한 번에 처리하면 개발자가 신경 쓸 요소를 줄일 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java/JPA</category>
      <category>JPA</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/87</guid>
      <comments>https://private-space.tistory.com/87#entry87comment</comments>
      <pubDate>Wed, 26 Feb 2020 01:12:10 +0900</pubDate>
    </item>
    <item>
      <title>Spring framework 소스코드 읽어보기 - Bean 생성 원리 (3)</title>
      <link>https://private-space.tistory.com/85</link>
      <description>&lt;h1&gt;Bean 생성 원리와 저장 위치&lt;/h1&gt;
&lt;p&gt;Spring에서 bean 생성 전략은 두 가지로 나뉜다.&lt;/p&gt;
&lt;p&gt;일반적으로 singleton을 사용하지만 상황에 따라 prototype bean을 사용하기도 한다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 Spring에서는 bean의 정의를 따로 저장해둔다.&lt;/p&gt;
&lt;p&gt;지난 포스트에선 딱 여기까지 진행했었다.&lt;/p&gt;
&lt;p&gt;이번엔 실제로 생성된 bean이 어디에 저장되는지를 탐구해보려고 한다.&lt;/p&gt;
&lt;h2&gt;소스 코드 작성&lt;/h2&gt;
&lt;p&gt;Spring framework의 코드만 보고 bean의 위치를 직접 찾아가기는 어렵다.&lt;/p&gt;
&lt;p&gt;목적에 맞는 소스 코드를 작성한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SomeBean&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @ToString
  public class SomeBean {
      private String name = &amp;quot;someBean&amp;quot;;
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;BeanConfiguration&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  public class BeanConfiguration {

      @Bean
      public SomeBean someBean() {
          return new SomeBean();
      }
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;main&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  public class DemoApplication {

      public static void main(String[] args) {
          ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfiguration.class);
          SomeBean someBean = (SomeBean) ctx.getBean(&amp;quot;someBean&amp;quot;);
          System.out.println(someBean.toString());
      }
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;추적&lt;/h2&gt;
&lt;p&gt;Annotation 기반의 컨텍스트를 생성했다(&lt;code&gt;AnnotationConfigApplicationContext&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;그리고 someBean을 getBean 메소드로 받아온다.&lt;/p&gt;
&lt;p&gt;getBean에 브레이크 포인트를 걸고 따라가보자.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AbstractApplicationContext.getBean(String name)&lt;/code&gt; 으로 이동된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public Object getBean(String name) throws BeansException {
    assertBeanFactoryActive();
    return getBeanFactory().getBean(name);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;getBean 메소드에선 beanFactory에 저장된 bean을 가져와서 리턴하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BeanFactory&lt;/code&gt;가 눈에 띈다.&lt;/p&gt;
&lt;p&gt;여기서 잠깐 &lt;code&gt;AnnotationConfigApplicationContext&lt;/code&gt;의 의존 관계 UML을 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHXgDK/btqCcALgT3Z/L00KJsY3M03a6fJCUpFED0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHXgDK/btqCcALgT3Z/L00KJsY3M03a6fJCUpFED0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHXgDK/btqCcALgT3Z/L00KJsY3M03a6fJCUpFED0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHXgDK%2FbtqCcALgT3Z%2FL00KJsY3M03a6fJCUpFED0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Spring framework 소스 코드를 받아와서 IDE로 열어보면 위와 같은 UML을 확인해볼 수 있다.&lt;/p&gt;
&lt;p&gt;참조 : &lt;a href=&quot;https://private-space.tistory.com/45&quot;&gt;Spring framework 소스 코드 읽어보기 첫 단계 - download, build&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;BeanFactory&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AnnotationConfigApplicationContext&lt;/code&gt;는 제일 하단에 있고 getBean 메소드는 조금 상단에 존재하는 &lt;code&gt;AbstractApplicationContext&lt;/code&gt; 클래스에 위치하고 있다.&lt;/p&gt;
&lt;p&gt;UML 사진 최상단에 &lt;code&gt;BeanFactory&lt;/code&gt; 인터페이스를 확인할 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ApplicationContext&lt;/code&gt;도 &lt;code&gt;BeanFactory&lt;/code&gt;의 하위에 존재하는 인터페이스이다.&lt;/p&gt;
&lt;p&gt;여튼 계속 추적해보자.&lt;/p&gt;
&lt;p&gt;getBeanFactory의 정의를 쫓아가면 &lt;code&gt;GenericApplicationContext&lt;/code&gt;의 필드에 &lt;code&gt;DefaultListableBeanFactory&lt;/code&gt; 타입으로 선언된 beanFactory를 볼 수 있다.&lt;/p&gt;
&lt;h3&gt;getBean&lt;/h3&gt;
&lt;p&gt;getBean 메소드는 &lt;code&gt;BeanFactory&lt;/code&gt;를 상속받는 &lt;code&gt;AbstractBeanFactory&lt;/code&gt; 클래스에 존재한다.&lt;/p&gt;
&lt;p&gt;아래는 getBean 메소드를 따라가면 나오는 doGetBean의 원본이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * Return an instance, which may be shared or independent, of the specified bean.
  * @param name the name of the bean to retrieve
  * @param requiredType the required type of the bean to retrieve
  * @param args arguments to use when creating a bean instance using explicit arguments
  * (only applied when creating a new instance as opposed to retrieving an existing one)
  * @param typeCheckOnly whether the instance is obtained for a type check,
  * not for actual use
  * @return an instance of the bean
  * @throws BeansException if the bean could not be created
  */
@SuppressWarnings(&amp;quot;unchecked&amp;quot;)
protected &amp;lt;T&amp;gt; T doGetBean(final String name, @Nullable final Class&amp;lt;T&amp;gt; requiredType,
        @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

    final String beanName = transformedBeanName(name);
    Object bean;

    // Eagerly check singleton cache for manually registered singletons.
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null &amp;amp;&amp;amp; args == null) {
        if (logger.isTraceEnabled()) {
            if (isSingletonCurrentlyInCreation(beanName)) {
                logger.trace(&amp;quot;Returning eagerly cached instance of singleton bean &amp;#39;&amp;quot; + beanName +
                        &amp;quot;&amp;#39; that is not fully initialized yet - a consequence of a circular reference&amp;quot;);
            }
            else {
                logger.trace(&amp;quot;Returning cached instance of singleton bean &amp;#39;&amp;quot; + beanName + &amp;quot;&amp;#39;&amp;quot;);
            }
        }
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }

    else {
        // Fail if we&amp;#39;re already creating this bean instance:
        // We&amp;#39;re assumably within a circular reference.
        if (isPrototypeCurrentlyInCreation(beanName)) {
            throw new BeanCurrentlyInCreationException(beanName);
        }

        // Check if bean definition exists in this factory.
        BeanFactory parentBeanFactory = getParentBeanFactory();
        if (parentBeanFactory != null &amp;amp;&amp;amp; !containsBeanDefinition(beanName)) {
            // Not found -&amp;gt; check parent.
            String nameToLookup = originalBeanName(name);
            if (parentBeanFactory instanceof AbstractBeanFactory) {
                return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
                        nameToLookup, requiredType, args, typeCheckOnly);
            }
            else if (args != null) {
                // Delegation to parent with explicit args.
                return (T) parentBeanFactory.getBean(nameToLookup, args);
            }
            else if (requiredType != null) {
                // No args -&amp;gt; delegate to standard getBean method.
                return parentBeanFactory.getBean(nameToLookup, requiredType);
            }
            else {
                return (T) parentBeanFactory.getBean(nameToLookup);
            }
        }

        if (!typeCheckOnly) {
            markBeanAsCreated(beanName);
        }

        try {
            final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
            checkMergedBeanDefinition(mbd, beanName, args);

            // Guarantee initialization of beans that the current bean depends on.
            String[] dependsOn = mbd.getDependsOn();
            if (dependsOn != null) {
                for (String dep : dependsOn) {
                    if (isDependent(beanName, dep)) {
                        throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                &amp;quot;Circular depends-on relationship between &amp;#39;&amp;quot; + beanName + &amp;quot;&amp;#39; and &amp;#39;&amp;quot; + dep + &amp;quot;&amp;#39;&amp;quot;);
                    }
                    registerDependentBean(dep, beanName);
                    try {
                        getBean(dep);
                    }
                    catch (NoSuchBeanDefinitionException ex) {
                        throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                &amp;quot;&amp;#39;&amp;quot; + beanName + &amp;quot;&amp;#39; depends on missing bean &amp;#39;&amp;quot; + dep + &amp;quot;&amp;#39;&amp;quot;, ex);
                    }
                }
            }

            // Create bean instance.
            if (mbd.isSingleton()) {
                sharedInstance = getSingleton(beanName, () -&amp;gt; {
                    try {
                        return createBean(beanName, mbd, args);
                    }
                    catch (BeansException ex) {
                        // Explicitly remove instance from singleton cache: It might have been put there
                        // eagerly by the creation process, to allow for circular reference resolution.
                        // Also remove any beans that received a temporary reference to the bean.
                        destroySingleton(beanName);
                        throw ex;
                    }
                });
                bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            }

            else if (mbd.isPrototype()) {
                // It&amp;#39;s a prototype -&amp;gt; create a new instance.
                Object prototypeInstance = null;
                try {
                    beforePrototypeCreation(beanName);
                    prototypeInstance = createBean(beanName, mbd, args);
                }
                finally {
                    afterPrototypeCreation(beanName);
                }
                bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
            }

            else {
                String scopeName = mbd.getScope();
                final Scope scope = this.scopes.get(scopeName);
                if (scope == null) {
                    throw new IllegalStateException(&amp;quot;No Scope registered for scope name &amp;#39;&amp;quot; + scopeName + &amp;quot;&amp;#39;&amp;quot;);
                }
                try {
                    Object scopedInstance = scope.get(beanName, () -&amp;gt; {
                        beforePrototypeCreation(beanName);
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        finally {
                            afterPrototypeCreation(beanName);
                        }
                    });
                    bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                }
                catch (IllegalStateException ex) {
                    throw new BeanCreationException(beanName,
                            &amp;quot;Scope &amp;#39;&amp;quot; + scopeName + &amp;quot;&amp;#39; is not active for the current thread; consider &amp;quot; +
                            &amp;quot;defining a scoped proxy for this bean if you intend to refer to it from a singleton&amp;quot;,
                            ex);
                }
            }
        }
        catch (BeansException ex) {
            cleanupAfterBeanCreationFailure(beanName);
            throw ex;
        }
    }

    // Check if required type matches the type of the actual bean instance.
    if (requiredType != null &amp;amp;&amp;amp; !requiredType.isInstance(bean)) {
        try {
            T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
            if (convertedBean == null) {
                throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
            }
            return convertedBean;
        }
        catch (TypeMismatchException ex) {
            if (logger.isTraceEnabled()) {
                logger.trace(&amp;quot;Failed to convert bean &amp;#39;&amp;quot; + name + &amp;quot;&amp;#39; to required type &amp;#39;&amp;quot; +
                        ClassUtils.getQualifiedName(requiredType) + &amp;quot;&amp;#39;&amp;quot;, ex);
            }
            throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
        }
    }
    return (T) bean;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;최하단에 &lt;code&gt;return (T) bean;&lt;/code&gt;이 보일 것이다.&lt;/p&gt;
&lt;p&gt;이 로직 내부에서 bean에 인스턴스를 저장하고 내보내는 것으로 추측된다.&lt;/p&gt;
&lt;p&gt;실제로 내용을 살펴보면 조건에 맞는 경우 bean에 인스턴스를 할당하고 넘기는 형태로 되어 있다.&lt;/p&gt;
&lt;p&gt;다시 위로 올라가서 살펴보면 첫 문장이 눈에 띈다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object sharedInstance = getSingleton(beanName);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;bean의 scope를 따로 정해주지 않았으니 당연히 singleton 타입일 것이다.&lt;/p&gt;
&lt;p&gt;저 메소드도 따라가보자.&lt;/p&gt;
&lt;h3&gt;getSingleton&lt;/h3&gt;
&lt;p&gt;이 메소드는 &lt;code&gt;DefaultSingletonBeanRegistry&lt;/code&gt;에 위치하고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * Return the (raw) singleton object registered under the given name.
  * &amp;lt;p&amp;gt;Checks already instantiated singletons and also allows for an early
  * reference to a currently created singleton (resolving a circular reference).
  * @param beanName the name of the bean to look for
  * @param allowEarlyReference whether early references should be created or not
  * @return the registered singleton object, or {@code null} if none found
  */
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null &amp;amp;&amp;amp; isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null &amp;amp;&amp;amp; allowEarlyReference) {
                ObjectFactory&amp;lt;?&amp;gt; singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;beanName으로 생성된 싱글턴 오브젝트를 리턴한다고 한다.&lt;/p&gt;
&lt;p&gt;그렇다면 첫 줄의 &lt;code&gt;singletonObjects&lt;/code&gt;이 무엇인지 확인해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** Cache of singleton objects: bean name to bean instance. */
private final Map&amp;lt;String, Object&amp;gt; singletonObjects = new ConcurrentHashMap&amp;lt;&amp;gt;(256);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이번에도 Map이다. bean의 이름 기반으로 저장된다는 것 같다.&lt;/p&gt;
&lt;p&gt;이 곳에 저장된 someBean을 받고 바로 doGetBean 메소드가 종료된다.&lt;/p&gt;
&lt;p&gt;일단 싱글톤 타입의 bean instance가 어느 위치에 저장되는지를 알아냈다.&lt;/p&gt;
&lt;p&gt;그렇다면 언제 생성해서 이 곳에 넣는지를 알아보자.&lt;/p&gt;
&lt;h3&gt;bean instance 저장 시점&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DefaultSingletonBeanRegistry&lt;/code&gt;의 필드, singletonObjects(Map)에 인스턴스가 저장된 것을 확인했다.&lt;/p&gt;
&lt;p&gt;이 클래스 내부에 &lt;code&gt;singletonObjects.put&lt;/code&gt;으로 검색하면 딱 두 군데가 나온다.&lt;/p&gt;
&lt;p&gt;모두 브레이크 포인트를 찍은 뒤 main문에 있는 브레이크 포인트는 제거해보자.&lt;/p&gt;
&lt;p&gt;put이 있는 지점에 몇 번 브레이크 포인트가 반복해서 걸린다.&lt;/p&gt;
&lt;p&gt;Spring이 시작되면 기본적으로 생성되는 bean이 있으니 &lt;strong&gt;beanName이 someBean인 곳&lt;/strong&gt;을 찾아야 한다.&lt;/p&gt;
&lt;p&gt;찾고 난 뒤 메소드를 계속 빠져나오다 보면 &lt;code&gt;AbstractApplicationContext.finishRefresh()&lt;/code&gt;에 커서가 위치한다.&lt;/p&gt;
&lt;p&gt;finishRefresh 메소드보다 한 단계 전에 실행하는 메소드에서 빈이 생성되었을 것이라고 추측할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // Prepare this context for refreshing.
        prepareRefresh();

        // Tell the subclass to refresh the internal bean factory.
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);

        try {
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);

            // Invoke factory processors registered as beans in the context.
            invokeBeanFactoryPostProcessors(beanFactory);

            // Register bean processors that intercept bean creation.
            registerBeanPostProcessors(beanFactory);

            // Initialize message source for this context.
            initMessageSource();

            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();

            // Initialize other special beans in specific context subclasses.
            onRefresh();

            // Check for listener beans and register them.
            registerListeners();

            // Instantiate all remaining (non-lazy-init) singletons.
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            finishRefresh();
        }

        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn(&amp;quot;Exception encountered during context initialization - &amp;quot; +
                        &amp;quot;cancelling refresh attempt: &amp;quot; + ex);
            }

            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();

            // Reset &amp;#39;active&amp;#39; flag.
            cancelRefresh(ex);

            // Propagate exception to caller.
            throw ex;
        }

        finally {
            // Reset common introspection caches in Spring&amp;#39;s core, since we
            // might not ever need metadata for singleton beans anymore...
            resetCommonCaches();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;finishRefresh 전에 실행되는 메소드는 finishBeanFactoryInitialization이다.&lt;/p&gt;
&lt;p&gt;finishBeanFactoryInitialization 메소드의 주석을 읽어보면 남은 모든 싱글톤을 초기화한다고 쓰여 있다.&lt;/p&gt;
&lt;p&gt;bean 인스턴스를 생성하고 저장하는 시점도 찾았다.&lt;/p&gt;
&lt;p&gt;들어가서 추적하다보면 또다시 getBean으로 진입하게 된다.&lt;/p&gt;
&lt;h2&gt;doGetBean&lt;/h2&gt;
&lt;p&gt;getBean을 추적하다보면 아까랑 똑같이 doGetBean으로 가게 된다.&lt;/p&gt;
&lt;p&gt;진짜 필요한 부분만 간추려보았다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected &amp;lt;T&amp;gt; T doGetBean(final String name, @Nullable final Class&amp;lt;T&amp;gt; requiredType,
        @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

    ...
    if (sharedInstance != null &amp;amp;&amp;amp; args == null) {
        ...
    } else {
        ...
        try {
            ...
            final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
            checkMergedBeanDefinition(mbd, beanName, args);
            ...
            // Create bean instance.
            if (mbd.isSingleton()) {
                sharedInstance = getSingleton(beanName, () -&amp;gt; {
                    try {
                        return createBean(beanName, mbd, args);
                    }
                    catch (BeansException ex) {
                        // Explicitly remove instance from singleton cache: It might have been put there
                        // eagerly by the creation process, to allow for circular reference resolution.
                        // Also remove any beans that received a temporary reference to the bean.
                        destroySingleton(beanName);
                        throw ex;
                    }
                });
                bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            }
            ...
        }
        catch (BeansException ex) {
            cleanupAfterBeanCreationFailure(beanName);
            throw ex;
        }
    }

    ...

    return (T) bean;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;인스턴스 mbd는 &lt;code&gt;RootBeanDefinition&lt;/code&gt; 타입의 변수인데, &lt;code&gt;RootBeanDefinition&lt;/code&gt;는 &lt;code&gt;BeanDefinition&lt;/code&gt;의 자손 클래스이다.&lt;/p&gt;
&lt;p&gt;그렇다면 bean의 정의를 담고 있는 것이다.&lt;/p&gt;
&lt;p&gt;조금 더 내리면 &lt;code&gt;if (mbd.isSingleton()) { ... }&lt;/code&gt;이 보인다.&lt;/p&gt;
&lt;p&gt;이름만 봐도 singleton이면 true를 리턴할 것처럼 생겼다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * Return whether this a &amp;lt;b&amp;gt;Singleton&amp;lt;/b&amp;gt;, with a single shared instance
  * returned from all calls.
  * @see #SCOPE_SINGLETON
  */
@Override
public boolean isSingleton() {
    return SCOPE_SINGLETON.equals(this.scope) || SCOPE_DEFAULT.equals(this.scope);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그렇다. 예상처럼 싱글톤 스코프인지 검사하는 메소드였다.&lt;/p&gt;
&lt;p&gt;싱글톤으로 선언했으니 이 if문 내부로 들어가야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sharedInstance = getSingleton(beanName, () -&amp;gt; {
    try {
        return createBean(beanName, mbd, args);
    }
    catch (BeansException ex) {
        // Explicitly remove instance from singleton cache: It might have been put there
        // eagerly by the creation process, to allow for circular reference resolution.
        // Also remove any beans that received a temporary reference to the bean.
        destroySingleton(beanName);
        throw ex;
    }
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;getSingleton의 인자로 beanName과 &lt;strong&gt;인터페이스의 구현체&lt;/strong&gt;를 넘겨주고 있다.&lt;/p&gt;
&lt;p&gt;그리고 그 구현체엔 createBean으로 bean을 생성하여 전달하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;createBean을 추적하다 보면 인스턴스는 &lt;code&gt;BeanWrapper&lt;/code&gt;로 한 번 포장하여 생성함을 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * Return the (raw) singleton object registered under the given name,
  * creating and registering a new one if none registered yet.
  * @param beanName the name of the bean
  * @param singletonFactory the ObjectFactory to lazily create the singleton
  * with, if necessary
  * @return the registered singleton object
  */
public Object getSingleton(String beanName, ObjectFactory&amp;lt;?&amp;gt; singletonFactory) {
    Assert.notNull(beanName, &amp;quot;Bean name must not be null&amp;quot;);
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            if (this.singletonsCurrentlyInDestruction) {
                throw new BeanCreationNotAllowedException(beanName,
                        &amp;quot;Singleton bean creation not allowed while singletons of this factory are in destruction &amp;quot; +
                        &amp;quot;(Do not request a bean from a BeanFactory in a destroy method implementation!)&amp;quot;);
            }
            if (logger.isDebugEnabled()) {
                logger.debug(&amp;quot;Creating shared instance of singleton bean &amp;#39;&amp;quot; + beanName + &amp;quot;&amp;#39;&amp;quot;);
            }
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet&amp;lt;&amp;gt;();
            }
            try {
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            catch (IllegalStateException ex) {
                // Has the singleton object implicitly appeared in the meantime -&amp;gt;
                // if yes, proceed with it since the exception indicates that state.
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    throw ex;
                }
            }
            catch (BeanCreationException ex) {
                if (recordSuppressedExceptions) {
                    for (Exception suppressedException : this.suppressedExceptions) {
                        ex.addRelatedCause(suppressedException);
                    }
                }
                throw ex;
            }
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 곳에서 맨 밑 부분에 있는 addSingleton 메소드 내부 로직엔 &lt;code&gt;singletonObjects.put&lt;/code&gt;이 들어 있다.&lt;/p&gt;
&lt;p&gt;새로운 싱글턴 인스턴스인 경우 여기서 생성하고 저장을 담당하는 것이다.&lt;/p&gt;
&lt;p&gt;Map 타입의 필드 singletonObjects에 인스턴스를 삽입하게 되면 bean 생성 과정은 종료된다.&lt;/p&gt;</description>
      <category>Java/Spring framework</category>
      <category>Spring</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/85</guid>
      <comments>https://private-space.tistory.com/85#entry85comment</comments>
      <pubDate>Sun, 23 Feb 2020 02:08:29 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 엔티티 매핑</title>
      <link>https://private-space.tistory.com/84</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;김영한 님의 자바 ORM 표준 JPA 프로그래밍을 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788960777330&amp;amp;orderClick=LEa&amp;amp;Kc=&quot;&gt;교보문고 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;엔티티 매핑&lt;/h1&gt;
&lt;p&gt;JPA에서 실제 테이블에 연결하는 객체를 &lt;strong&gt;엔티티&lt;/strong&gt;라고 부른다.&lt;/p&gt;
&lt;p&gt;따라서 엔티티와 테이블을 정확히 매핑시켜 사용하는 것이 매우 중요하다.&lt;/p&gt;
&lt;p&gt;아래는 매핑을 위한 대표적인 Annotation 목록이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;객체와 테이블 매핑&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Entity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Table&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기본 키 매핑&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;필드와 컬럼 매핑&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Column&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;연관관계 매핑&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@ManyToOne&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@JoinColumn&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;@Entity&lt;/h2&gt;
&lt;p&gt;테이블과 매핑할 클래스엔 &lt;code&gt;@Entity&lt;/code&gt;가 반드시 붙어야 한다.&lt;/p&gt;
&lt;h3&gt;속성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;name (Default : 클래스명)&lt;ul&gt;
&lt;li&gt;entity에 사용할 이름&lt;/li&gt;
&lt;li&gt;엔티티 중복은 허용되지 않는다.&lt;/li&gt;
&lt;li&gt;entity의 이름과 테이블 이름은 다른 개념이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;주의사항&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;기본 생성자는 반드시 존재해야 하며 public/proteced 접근자를 가져야 한다.&lt;ul&gt;
&lt;li&gt;자바는 기본 생성자를 만들지 않으면 빈 생성자를 자동으로 생성한다.&lt;/li&gt;
&lt;li&gt;그러나 별도의 생성자를 정의할 경우 기본 생성자가 만들어지지 않는다.&lt;/li&gt;
&lt;li&gt;그럴 땐 반드시 기본 생성자를 명시적으로 정의해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;final, enum 클래스, 내부 클래스, 인터페이스엔 사용 불가능&lt;/li&gt;
&lt;li&gt;저장할 필드에 final 사용 불가능&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;@Table&lt;/h2&gt;
&lt;p&gt;엔티티와 매핑할 테이블을 지정한다.&lt;/p&gt;
&lt;p&gt;생략 시 엔티티 이름이 테이블 이름으로 사용된다.&lt;/p&gt;
&lt;h3&gt;@Table 속성&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;name (Default : 엔티티명)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;매핑할 테이블 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;catalog&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;catalog 기능이 있는 DB에서 catalog 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;schema&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;schema 기능이 있는 DB에서 schema 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;uniqueConstraints&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DDL 생성 시 유니크 제약조건 생성&lt;/li&gt;
&lt;li&gt;복합 제약조건 생성 가능&lt;/li&gt;
&lt;li&gt;JPA의 스키마 자동 생성 기능을 사용하여 DDL을 만들 때에만 적용됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;스키마 자동 생성하기&lt;/h2&gt;
&lt;p&gt;JPA는 엔티티와 테이블을 매핑하고 DB 스키마를 자동으로 생성한다.&lt;/p&gt;
&lt;p&gt;Spring properties에 아래의 속성을 입력한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
    jpa:
        hibernate:
            ddl-auto: create
            show-sql: true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ddl-auto 속성은 엔티티를 읽어 테이블을 자동으로 생성한다.&lt;/p&gt;
&lt;p&gt;show-sql은 어플리케이션에서 JPA를 통해 사용한 SQL문을 로그로 뿌려주는 기능이다.&lt;/p&gt;
&lt;p&gt;Entity 클래스 &lt;code&gt;Member&lt;/code&gt;가 정의되어 있다고 가정하고, Spring boot 어플리케이션을 실행한다면 아래와 같은 로그를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hibernate: drop table MEMBER if exists
Hibernate: create table MEMBER ( ... )&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;drop을 먼저 실행하고 테이블을 생성했다.&lt;/p&gt;
&lt;p&gt;ddl-auto 속성이 create인 경우 어플리케이션 시작 시 모든 테이블을 삭제하고 다시 만들기 때문에 drop을 실행한 것이다.&lt;/p&gt;
&lt;h3&gt;ddl-auto 옵션&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;create&lt;ul&gt;
&lt;li&gt;위의 예제처럼 테이블을 모두 삭제한 뒤 다시 생성한다.&lt;/li&gt;
&lt;li&gt;DROP → CREATE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;create-drop&lt;ul&gt;
&lt;li&gt;create 속성을 포함한다.&lt;/li&gt;
&lt;li&gt;애플리케이션 종료 시 생성한 DDL도 제거한다.&lt;/li&gt;
&lt;li&gt;DROP → CREATE → DROP&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;update&lt;ul&gt;
&lt;li&gt;DB 테이블과 엔티티 매핑정보를 비교한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;변경 사항&lt;/strong&gt;만 수정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;validate&lt;ul&gt;
&lt;li&gt;DB 테이블과 엔티티 매핑 정보를 비교한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;변경 사항&lt;/strong&gt;이 있는 경우 &lt;strong&gt;경고&lt;/strong&gt;를 발생시키고 어플리케이션을 실행시키지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;none&lt;ul&gt;
&lt;li&gt;자동 생성 기능을 사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;ddl-auto 옵션 전략&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;운영 서버에서는 DDL 수정 옵션을 절대 사용하지 않는다&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;create, create-drop, update 절대 금지&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개발 초기 단계&lt;ul&gt;
&lt;li&gt;create, update 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;초기화 상태로 자동화된 테스트를 진행하는 환경 / CI 서버&lt;ul&gt;
&lt;li&gt;create, create-drop 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트 서버&lt;ul&gt;
&lt;li&gt;update, validate 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스테이징/운영 서버&lt;ul&gt;
&lt;li&gt;validate, none 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;기본 키 매핑&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@Id&lt;/code&gt;를 사용하면 테이블의 기본 키를 매핑할 수 있다.&lt;/p&gt;
&lt;h3&gt;기본 키 할당 전략&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;직접 할당&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;애플리케이션에서 직접 할당한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  Board board = new Board();
  board.setId(1);
  boardRepository.save(board);&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;자동 생성&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@GeneratedValue&lt;/code&gt;를 사용하여 키를 자동으로 생성한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;strategy 옵션을 활용하여 자동 생성 전략을 정의할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;GenerationType.IDENTITY&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DB에 위임하는 방법이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MySQL의 AUTO_INCREMENT와 같은 기능을 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GenerationType.SEQUENCE&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;유일한 값을 순서대로 생성하는 전략으로, DB에서 지원해야 가능하다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;오라클, PostgreSQL, DB2, H2에서 사용 가능하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Entity
  @SequenceGenerator(
      name = &amp;quot;BOARD_SEQ_GENERATOR&amp;quot;,
      sequenceName = &amp;quot;BOARD_SEQ&amp;quot;,
      initialValue = 1, allocationSize = 1)
  public class Board {
      @Id
      @GeneratedValue(strategy = GenerationType.SEQUENCE,
                      generator = &amp;quot;BOARD_SEQ_GENERATOR&amp;quot;)
      private Long id;
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@SequenceGenerator&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;name : 식별자 생성기 (필수)&lt;/li&gt;
&lt;li&gt;sequenceName : DB에 등록된 시퀀스 이름&lt;/li&gt;
&lt;li&gt;initialValue : 시퀀스 DDL을 생성할 때 시작할 수를 정함 (Default 1)&lt;/li&gt;
&lt;li&gt;allocationSize : 시퀀스 한 번 호출에 증가하는 수&lt;/li&gt;
&lt;li&gt;catalog, schema : DB의 catalog, schema 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GenerationType.TABLE&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;키 생성 전용 테이블을 만들어 여기에 이름과 값으로 사용할 컬럼을 만든다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;위의 시퀀스를 흉내내는 전략이다. 모든 DB에서 사용 가능하다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;테이블&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Entity
  @TableGenerator( // 테이블 키 생성기를 등록한다.
      name = &amp;quot;BOARD_SEQ_GENERATOR&amp;quot;,
      table = &amp;quot;MY_SEQUENCES&amp;quot;,
      pkColumnValue = &amp;quot;BOARD_SEQ&amp;quot;, allocationSize = 1)
  public class Board {
      @Id
      @GeneratedValue(strategy = GenerationType.TABLE,
                      generator = &amp;quot;BOARD_SEQ_GENERATOR&amp;quot;)
      private Long id;
  }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@TableGenerator&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;name : 식별자 생성기 (필수)&lt;/li&gt;
&lt;li&gt;table : 키 생성 테이블명&lt;/li&gt;
&lt;li&gt;pkColumnName : 시퀀스 컬럼명&lt;/li&gt;
&lt;li&gt;valueColumnName : 시퀀스 값 컬럼명&lt;/li&gt;
&lt;li&gt;pkCOlumnValue : 키로 사용할 값 이름&lt;/li&gt;
&lt;li&gt;initialValue : 초기값 (Default 0)&lt;/li&gt;
&lt;li&gt;allocationSize : 시퀀스 한 번 호출에 증가하는 수&lt;/li&gt;
&lt;li&gt;catalog, schema : DB의 catalog, schema 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GenerationType.AUTO&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;선택한 데이터베이스의 방언에 따라 IDENTITY, SEQUENCE, TABLE 중 하나를 자동으로 선택한다.&lt;/li&gt;
&lt;li&gt;오라클인 경우 SEQUENCE, MySQL인 경우 IDENTITY를 선택한다.&lt;/li&gt;
&lt;li&gt;데이터베이스를 변경해도 코드 수정이 필요 없다는 장점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;필드와 컬럼 매핑&lt;/h2&gt;
&lt;p&gt;아래는 컬럼과 매핑되는 대표적인 Annotation의 종류이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@Column&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;일반적인 컬럼 매핑용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Enumerated&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;enum 타입 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Temporal&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;날짜 타입 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Lob&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;BLOB, CLOB 타입 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transient&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;매핑하지 않는 필드 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Access&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;JPA가 엔티티에 접근하는 방식 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;@Column&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@Column&lt;/code&gt;은 객체 필드를 테이블 컬럼과 매핑한다.&lt;/p&gt;
&lt;p&gt;가장 일반적으로 사용된다.&lt;/p&gt;
&lt;p&gt;아래는 속성 목록이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;name (Default 필드 이름)&lt;ul&gt;
&lt;li&gt;테이블의 컬럼 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;insertable (Default true)&lt;ul&gt;
&lt;li&gt;엔티티 저장 시 이 필드도 같이 저장&lt;/li&gt;
&lt;li&gt;false로 설정 시 이 필드를 DB에 저장하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;updatable (Default true)&lt;ul&gt;
&lt;li&gt;엔티티 수정 시 이 필드도 같이 수정&lt;/li&gt;
&lt;li&gt;false로 설정 시 DB 수정하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;table&lt;ul&gt;
&lt;li&gt;하나의 엔티티를 두 개 이상의 테이블에 매핑할 떄 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;nullable (Default true)&lt;ul&gt;
&lt;li&gt;false로 설정 시 이 필드에 NOT NULL 옵션 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;unique&lt;ul&gt;
&lt;li&gt;유니크 제약조건 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;columnDefinition&lt;ul&gt;
&lt;li&gt;DB의 컬럼 정보를 직접 줄 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;length&lt;ul&gt;
&lt;li&gt;문자 길이 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;precision, scale&lt;ul&gt;
&lt;li&gt;BigDecimal, BigInteger 등에서 사용&lt;/li&gt;
&lt;li&gt;precision은 소수점을 포함한 전체 자리수를 명시한다&lt;/li&gt;
&lt;li&gt;scale은 소수의 자리수를 명시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;@Enumerated&lt;/h3&gt;
&lt;p&gt;enum 타입을 매핑할 때 사용한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;enum 정의&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; enum LogType {
     CREATE, READ, UPDATE, DELETE
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;enum 사용&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; // 필드 정의
 @Enumerated(EnumType.STRING)
 private LogType logType;

 // 실제 사용
 logType.setLogType(LogType.READ);&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;@Enumerated&lt;/code&gt;의 속성은 value가 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EnumType.ORDINAL&lt;/code&gt;을 사용하면 enum 순서를 DB에 저장하고 &lt;code&gt;EnumType.STRING&lt;/code&gt;을 사용하면 enum 이름을 데이터베이스에 저장한다.&lt;/p&gt;
&lt;h3&gt;@Temporal&lt;/h3&gt;
&lt;p&gt;날짜 타입을 매핑할 때 사용한다. 생략 시 timestamp 형식으로 자동 저장된다.&lt;/p&gt;
&lt;p&gt;옵션으로 형식을 정해주어야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TemporalType.DATE&lt;ul&gt;
&lt;li&gt;날짜 (2020-02-22) 저장&lt;/li&gt;
&lt;li&gt;DB의 date 타입과 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TemporalType.TIME&lt;ul&gt;
&lt;li&gt;시간 (16:50:30) 저장&lt;/li&gt;
&lt;li&gt;DB의 time 형식에 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TemporalType.TIMESTAMP&lt;ul&gt;
&lt;li&gt;날짜와 시간 (2020-02-22 16:50:30) 저장&lt;/li&gt;
&lt;li&gt;DB의 timestamp에 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;@Lob&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@Lob&lt;/code&gt; 에는 속성을 정하지 않는다.&lt;/p&gt;
&lt;p&gt;매핑 필드 타입이 문자면 CLOB, 나머지는 BLOB로 자동 설정된다.&lt;/p&gt;
&lt;h2&gt;@Transient&lt;/h2&gt;
&lt;p&gt;데이터베이스에 저장되지도 않고 불러올 수도 없다.&lt;/p&gt;
&lt;p&gt;객체에 임시로 값을 저장하고자 할 때 사용하기 적합하다.&lt;/p&gt;
&lt;h3&gt;@Access&lt;/h3&gt;
&lt;p&gt;JPA가 엔티티 데이터에 접근하는 방식을 지정한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;필드로 접근하기 (AccessType.FIELD)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;객체 필드에 직접 접근하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;프로퍼티로 접근하기 (AccessType.PROPERTY)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;getter 메소드에 접근하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;사용 예시를 보면 쉽게 이해할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;AccessType.FIELD&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     @Entity
     @Access (AccessType.FIELD)
     public class Member {
         @Id
         private Long id;

         @Column
         private String name;
     }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AccessType.PROPERTY&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 @Access (AccessType.PROPERTY)
 public class Member {
     private Long id;
     private String name;

     @Id
     public Long getId() { return id; }
     @Column
     public String getName() { return name; }
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;같이 사용 (getter에 특별한 로직이 필요한 경우 유용)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Entity
 public class Member {
     @Id
     private Long id;
     private String name;

     @Column
     public String getName() { return name; }
 }&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Java/JPA</category>
      <category>JPA</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/84</guid>
      <comments>https://private-space.tistory.com/84#entry84comment</comments>
      <pubDate>Sat, 22 Feb 2020 17:16:21 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Code] 동시성</title>
      <link>https://private-space.tistory.com/83</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;Clean Code를 읽고 내용을 정리한 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788966260959&amp;amp;orderClick=LEA&amp;amp;Kc=&quot;&gt;책 구매하러 가기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;동시성&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;객체는 처리의 추상화다. 스레드는 일정의 추상화다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;동시성과 깔끔한 코드는 양립하기 어렵다.&lt;/p&gt;
&lt;p&gt;단일 스레드 프로그래밍은 상대적으로 쉬운 편에 속한다.&lt;/p&gt;
&lt;p&gt;다중 스레드 코드도 맘만 먹으면 코드를 쉽게 작성할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 쉽게 작성한 다중 스레드 코드는 시스템의 부하가 큰 환경에서 코드 내부 깊숙한 곳부터 발생한 문제가 있을 수 있다.&lt;/p&gt;
&lt;h2&gt;동시성의 필요성&lt;/h2&gt;
&lt;p&gt;동시성은 &lt;strong&gt;무엇&lt;/strong&gt;과 &lt;strong&gt;언제&lt;/strong&gt;를 분리하는 전략이다.&lt;/p&gt;
&lt;p&gt;단일 스레드 프로그램을 생각해보자.&lt;/p&gt;
&lt;p&gt;Breakpoint를 걸어놓기만 하면 언제 무엇을 하는지 명확하게 잡아낼 수 있다.&lt;/p&gt;
&lt;p&gt;무엇과 언제를 분리하면 프로그램 구조와 효율이 극적으로 나아진다.&lt;/p&gt;
&lt;p&gt;구조적인 관점에선 거대한 루프 하나가 아니라 작은 단위의 프로그램 여러 개가 협업하는 것과 같기 때문에 시스템을 이해하기 쉽고 문제를 분리하기도 쉽다.&lt;/p&gt;
&lt;p&gt;동시성을 채택하는 이유는 여러 가지가 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;각 스레드에게 독립적인 영역을 할당해야 하는 경우&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서블릿은 요청이 들어오면 비동기적으로 명령을 수행하고, 다른 서블릿 스레드와는 무관하게 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Response time / Throughput 개선&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;크롤러 프로그램이 한 번에 한 웹 사이트를 방문하여 정보를 가져온다고 할 때,&lt;/li&gt;
&lt;li&gt;방문할 웹사이트를 추가한 만큼 정보를 수집하는 시간도 늘어나게 된다.&lt;/li&gt;
&lt;li&gt;단일 스레드인 경우 한 번에 한 페이지만을 방문하겠지만 다중 스레드 알고리즘을 활용하면 여러 페이지를 동시에 방문하여 수집 성능을 높일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결론은 병렬 처리가 필요한 때 동시성을 채택한다는 것이다.&lt;/p&gt;
&lt;h3&gt;동시성에 대한 오해&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;동시성은 항상 더 좋은 처리 성능을 보여준다?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여러 프로세서가 동시에 처리해야 하는 독립적인 명령이 충분히 많은 경우에만 성능이 향상된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;동시성을 구현해도 설계는 그대로다?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일/다중 스레드 시스템은 설계부터 매우 다르다.&lt;/li&gt;
&lt;li&gt;일반적으로 &lt;strong&gt;무엇&lt;/strong&gt;과 &lt;strong&gt;언제&lt;/strong&gt;를 분리하면 시스템 구조가 크게 달라진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;웹 컨테이너를 사용하면 동시성을 이해하지 않아도 된다?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;컨테이너의 동작 방식을 이해해야 더 효율적인 시스템을 만들 수 있다.&lt;/li&gt;
&lt;li&gt;공유 메모리나 데드락과 같은 전형적인 동시성 문제를 어떻게 피할 수 있는지 알아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;반대로 다음은 동시성에 대해서 타당한 의견이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;동시성은 부하를 유발한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;성능 측면에서 부하가 걸리고 일반적으로 코드도 더 길다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;동시성은 복잡하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;간단한 문제도 해결하기 어려울 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;동시성 버그는 재현하기 어렵다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;재현의 어려움으로 인해 결함이 아니라 일시적 문제로 여길 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;동시성을 구현하려면 근본적인 설계 전략을 세워야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;난관&lt;/h2&gt;
&lt;p&gt;동시성이 어려운 이유를 알아보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Document {
    private int readCount;

    public int getReadCountAndPlus() {
        return ++readCount;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드를 두 개의 스레드가 동시에 접근하면 이상한 값이 튀어나온다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;스레드 1이 readCount = 43일 때 getReadCountAndPlus를 실행한다.&lt;/li&gt;
&lt;li&gt;스레드 2가 readCount = 43일 때 getReadCountAndPlus를 실행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;당연히 readCount의 값은 44가 되어야 할 것 같은데, 실제로는 43이 나온다.&lt;/p&gt;
&lt;p&gt;코드로만 보면 대체 어느 경로로 진입하기에 저런 결과가 나올까? 하는 생각을 할 수 있다.&lt;/p&gt;
&lt;p&gt;바이트 코드만 고려했을 때 두 스레드가 &lt;strong&gt;getReadCountAndPlus&lt;/strong&gt;를 실행하는 경로는 최대 12,870개나 된다.&lt;/p&gt;
&lt;p&gt;readCount를 int 대신 long으로 사용하면 경우의 수가 2,704,156개로 증가한다.&lt;/p&gt;
&lt;h2&gt;동시성 방어 원칙&lt;/h2&gt;
&lt;h3&gt;단일 책임 원칙 (Single Responsibility Principle)&lt;/h3&gt;
&lt;p&gt;동시성은 복잡하기 때문에 다른 코드와 &lt;strong&gt;분리&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동시성 코드는 독자적인 개발, 변경, 조율 주기가 있어야 한다.&lt;/li&gt;
&lt;li&gt;동시성 코드에서는 단일 스레드 코드에서 겪지 못하는 난관이 있다.&lt;/li&gt;
&lt;li&gt;잘못 구현한 동시성 코드는 상상 이상으로 다양하게 실패한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;데이터를 최대한 제한하기&lt;/h3&gt;
&lt;p&gt;자바는 &lt;code&gt;synchronized&lt;/code&gt; 키워드로 임계구역을 보호할 수 있다.&lt;/p&gt;
&lt;p&gt;이하는 임계구역을 최대한 줄여야 하는 이유이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;synchronized&lt;/code&gt; 키워드를 잊어버려 데이터를 수정하는 로직이 망가질 수 있다.&lt;/li&gt;
&lt;li&gt;모든 임계구역을 보호했는지 확인하는 데에 시간이 소모된다.&lt;/li&gt;
&lt;li&gt;임계구역에서 발생하는 버그는 찾아내기 어렵다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;데이터를 복사하여 사용하기&lt;/h3&gt;
&lt;p&gt;공유 데이터를 줄이는 제일 좋은 방법은 하나도 사용하지 않는 것이다.&lt;/p&gt;
&lt;p&gt;공유하는 대신 원본 객체를 복사해서 사용하는 방법을 고려해보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;임계구역 보호에 소모되는 비용&lt;/li&gt;
&lt;li&gt;사본을 생성하고 GC에 드는 비용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;후자가 전자보다 나은 경우가 더 많다.&lt;/p&gt;
&lt;h3&gt;스레드는 가급적 독립적으로 구현하라&lt;/h3&gt;
&lt;p&gt;독립적인 스레드는 몇 가지 조건을 충족한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;다른 스레드와 데이터를 공유하지 않는다.&lt;/li&gt;
&lt;li&gt;각 스레드는 클라이언트의 요청 하나를 처리한다.&lt;/li&gt;
&lt;li&gt;공유되지 않은 인스턴스 데이터만을 사용하고 로컬 변수로 저장한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;실행 모델의 이해&lt;/h2&gt;
&lt;p&gt;기본 용어를 먼저 살펴본다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;한정된 자원&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;크기나 숫자가 제한적인 자원으로, DB Connection, 입력 길이가 일정한 Read/Write 버퍼 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;상호 배제&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 번에 한 스레드만 공유 데이터/자원에 접근할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;기아 현상&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;특정 스레드가 자원을 오랫동안 혹은 영원히 기다리는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;데드락&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여러 스레드가 자원을 붙잡고 놓지 않으면서 서로가 끝나기를 기다리는 상태&lt;/li&gt;
&lt;li&gt;필요한 자원이 모두 점유되어 더 이상 작업을 진행할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;라이브락&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;락을 거는 단계에서 각 스레드가 서로를 방해&lt;/li&gt;
&lt;li&gt;스레드는 계속 진행하려하나, 굉장히 오랫동안 혹은 영원히 진행하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;생산자-소비자 모델&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;생산자&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;생산자 스레드는 데이터를 만들어서 특정 저장소(버퍼나 큐)에 삽입한다.&lt;/li&gt;
&lt;li&gt;저장소가 꽉 차면 데이터를 채울 수 없기 때문에 빌 때까지 기다린다.&lt;/li&gt;
&lt;li&gt;대기열에 데이터를 채우면 소비자 스레드에게 &amp;quot;대기열에 데이터가 있다&amp;quot;는 시그널을 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;소비자&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소비자는 만들어진 데이터를 저장소에서 가져와 사용한다.&lt;/li&gt;
&lt;li&gt;저장소가 비면 데이터를 가져올 수 없기 때문에 찰 때까지 기다린다.&lt;/li&gt;
&lt;li&gt;대기열에서 데이터를 읽어들이면 생산자 스레드에게 &amp;quot;대기열에 빈 공간이 있다&amp;quot;는 시그널을 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;설계가 잘못될 경우 동시에 서로의 시그널을 기다릴 가능성이 존재한다.&lt;/p&gt;
&lt;h3&gt;읽기-쓰기&lt;/h3&gt;
&lt;p&gt;읽기 스레드(Reader Thread)와 쓰기 스레드(Writer Thread)가 공유 데이터를 사용하고 있다.&lt;/p&gt;
&lt;p&gt;쓰기 쓰레드는 공유 데이터를 갱신한다.&lt;/p&gt;
&lt;p&gt;이런 경우, &lt;strong&gt;처리율(Throughput)&lt;/strong&gt;이 문제의 핵심이다.&lt;/p&gt;
&lt;p&gt;처리율 강조 시 기아 현상이 생기거나 오래된 데이터가 쌓인다.&lt;/p&gt;
&lt;p&gt;갱신을 허용하면 처리율에 영향을 미친다.&lt;/p&gt;
&lt;p&gt;처리율도 적당히 높이고 기아도 방지하는 해법이 필요하다.&lt;/p&gt;
&lt;h4&gt;간단한 전략&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;읽기 스레드가 없을 때까지 갱신을 원하는 쓰기 스레드가 버퍼를 기다리는 방법&lt;ul&gt;
&lt;li&gt;읽기 스레드가 계속 작업을 이어가면 쓰기 스레드는 기아 상태에 빠진다.&lt;/li&gt;
&lt;li&gt;쓰기 스레드의 우선순위를 높인 상태에서 쓰기 스레드가 계속 작업을 이어가면 처리율이 떨어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;식사하는 철학자들&lt;/h3&gt;
&lt;p&gt;둥근 원탁에 둘러 앉은 철학자의 예시로, 운영체제를 공부해본 사람이라면 아는 바로 그 예시이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;전제 조건&lt;ul&gt;
&lt;li&gt;각 철학자의 왼쪽에 포크가 놓여 있다.&lt;/li&gt;
&lt;li&gt;식탁 가운데에는 스파게티가 있다.&lt;/li&gt;
&lt;li&gt;철학자는 양 손에 포크를 잡아야만 식사를 할 수 있다.&lt;/li&gt;
&lt;li&gt;사용 중인 포크는 식사를 마치고 포크를 내려 놓기 전까진 다시 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;스파게티를 먹고 나면 배가 고플 때까지 다시 생각에 잠긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 철학자를 스레드로, 포크를 자원으로 바꿔서 생각해보자.&lt;/p&gt;
&lt;p&gt;애플리케이션은 여러 프로세스가 자원을 얻기 위해 경쟁한다.&lt;/p&gt;
&lt;p&gt;설계 오류로 인해 데드락/라이브락/처리율/효율성 저하와 같은 문제가 발생할 수 있다.&lt;/p&gt;
&lt;h2&gt;동기화하는 메소드 사이의 의존성&lt;/h2&gt;
&lt;p&gt;동기화하는 메소드 사이에 의존성이 존재하면 찾기 어려운 버그가 생긴다.&lt;/p&gt;
&lt;p&gt;공유 클래스 하나에 동기화 메소드가 여러개라면 설계부터 다시 검토해보자.&lt;/p&gt;
&lt;p&gt;공유 객체 하나에는 메소드 하나만 사용하는 것이 바람직하다.&lt;/p&gt;
&lt;p&gt;그 이상 사용하고자 한다면 각 메소드나 서버를 잠그는 별도의 로직이 존재해야 한다.&lt;/p&gt;
&lt;h2&gt;임계구역은 최대한 줄일 것&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;synchronized&lt;/code&gt; 키워드는 락을 만들 수 있다.&lt;/p&gt;
&lt;p&gt;락으로 감싼 영역은 한 번에 한 스레드만 실행이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;synchronized&lt;/code&gt;의 남발은 속도 저하를 야기할 수 있는 바람직하지 못한 코드이지만,&lt;/p&gt;
&lt;p&gt;임계구역이 보호받지 않으면 예상치 못한 버그가 발생한다.&lt;/p&gt;
&lt;p&gt;따라서 코드를 짤 때는 임계구역의 수를 가능한 한 줄일 수 있게 노력하자.&lt;/p&gt;
&lt;h2&gt;올바른 종료 코드&lt;/h2&gt;
&lt;p&gt;스레드를 깔끔하게 종료하는 코드는 올바로 구현하기 어렵다.&lt;/p&gt;
&lt;p&gt;제대로 구현되지 않으면 데드락 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 생산자-소비자 스레드가 하나씩 있을 때, 생산자만 종료한다면 소비자는 생산자의 메시지를 영원히 기다리는 상태에 빠지게 된다.&lt;/p&gt;
&lt;h2&gt;스레드 테스트&lt;/h2&gt;
&lt;p&gt;스레드 테스트는 고려 사항이 많다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;말도 안 되는 실패는 스레드 문제로 취급할 것.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다중 스레드는 가끔 이해할 수 없는 버그를 일으킬 때가 있다.&lt;/li&gt;
&lt;li&gt;재현하기가 매우 어렵지만, 단순한 &lt;strong&gt;일회성 버그로 치부하고 무시하면 안된다&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;일회성 문제를 무시하고 계속 코드를 작성하면 잘못된 코드 위에 계속 코드가 쌓여간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다중 스레드를 고려하지 않은 코드부터 먼저 최대한 완벽하게 만들 것.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스레드 환경 밖에서 잘 돌아가는지 점검하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다중 스레드 코드 영역이 다양한 환경에 잘 끼워넣을 수 있게 구현할 것.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실행하는 스레드의 수를 1, N, 실행 중 스레드 수를 바꾸는 방법 등을 사용해보자.&lt;/li&gt;
&lt;li&gt;실제 환경/테스트 환경에서 모두 돌려본다.&lt;/li&gt;
&lt;li&gt;반복 테스트가 가능한 테스트 케이스를 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다중 스레드를 쓰는 코드를 상황에 맞춰 조정할 수 있게 작성할 것.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;적절한 스레드의 개수를 파악하는 데에는 시행착오가 필요하다.&lt;/li&gt;
&lt;li&gt;프로그램의 성능을 측정하여 스레드 개수를 조율해본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;프로세서 수보다 많은 스레드를 돌려볼 것.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스레드 스와핑 시에 문제가 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;프로세서 수보다 많은 스레드를 사용하면 스와핑을 강제로 일으킬 수 있다.&lt;/li&gt;
&lt;li&gt;스와핑이 잦으면 임계구역 문제나 데드락 문제를 발견하기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;다른 플랫폼에서 돌려볼 것.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mac, Linux, Windows 등 운영체제마다 스레드 처리 정책이 다르다.&lt;/li&gt;
&lt;li&gt;다중 스레드 코드는 플랫폼에 따라 다르게 실행될 여지가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;무조건 실패하는 코드를 넣어 테스트해볼 것.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스레드 내에서 발생한 오류는 찾기 어렵다.&lt;/li&gt;
&lt;li&gt;수천, 수만가지 경로 중 극소수만 실패하기 때문이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object.wait()&lt;/code&gt;, &lt;code&gt;Object.sleep()&lt;/code&gt; 등 스레드 제어 코드를 넣어 다양한 순서로 실행하며 버그를 찾아보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>IT 도서/Clean Code</category>
      <category>Clean Code</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/83</guid>
      <comments>https://private-space.tistory.com/83#entry83comment</comments>
      <pubDate>Thu, 20 Feb 2020 01:26:03 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Code] 창발성</title>
      <link>https://private-space.tistory.com/82</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;Clean Code를 읽고 내용을 정리한 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788966260959&amp;amp;orderClick=LEA&amp;amp;Kc=&quot;&gt;책 구매하러 가기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;창발성 (創發性)&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;하위 체계로부터 생겨나지만, 그 하위 체계로 환원되지 않는 속성&lt;/p&gt;
&lt;p&gt;하위 계층(구성 요소)에는 없는 특성이나 행동이 상위 계층(전체 구조)에서 자발적으로 돌연히 출현하는 현상&lt;/p&gt;
&lt;p&gt;조직(organization)의 일정수준에서 실체에 속한 성질은 그보다 낮은 차원에서 발견된 성질로부터는 예견할 수 없다는 것&lt;/p&gt;
&lt;p&gt;즉, 하위 요소를 잘 결합하여 전혀 다른 결과를 얻어 내는 것을 말한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SW 설계 품질을 높여주는 켄트 벡의 &lt;b&gt;단순한 설계&lt;/b&gt; 규칙 4가지 (중요도 순)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;모든 테스트 실행&lt;/li&gt;
&lt;li&gt;중복 제거&lt;/li&gt;
&lt;li&gt;개발자의 의도를 표현&lt;/li&gt;
&lt;li&gt;클래스/메소드의 수를 최소한으로&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;모든 테스트 실행하기&lt;/h2&gt;
&lt;p&gt;SW 설계는 개발자의 의도가 담겨있다.&lt;/p&gt;
&lt;p&gt;테스트를 거친 시스템은 개발자의 의도를 검증한 개발 산출물이다.&lt;/p&gt;
&lt;p&gt;테스트가 가능한 시스템을 만드려고 노력하면 설계의 품질도 높아질 수밖에 없다.&lt;/p&gt;
&lt;p&gt;이유를 나열해보면 아래와 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;단일 책임 원칙을 지키는 클래스가 테스트하기 편하다.&lt;/li&gt;
&lt;li&gt;테스트 케이스가 많을수록 개발자는 코드를 작성하기 쉬워진다.&lt;/li&gt;
&lt;li&gt;결합도가 높으면 테스트를 작성하기 어렵다. 의존 관계 역전 법칙을 준수하고 추상화 기법과 같은 방법을 활용하면 결합도를 낮출 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;따라서 테스트를 철저히 수행할 수 있는 시스템은 곧 좋은 품질의 설계로 이어진다.&lt;/p&gt;
&lt;h2&gt;Refactoring&lt;/h2&gt;
&lt;p&gt;코드와 테스트 케이스까지 모두 작성을 완료했으면 정리를 할 타이밍이다.&lt;/p&gt;
&lt;p&gt;테스트 케이스가 작성되어 있기 때문에 코드 변경을 두려워하지 않아도 된다.&lt;/p&gt;
&lt;p&gt;리팩토링 단계에선 SW 설계 품질을 충분히 끌어올려야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;응집도를 높인다.&lt;/li&gt;
&lt;li&gt;결합도를 낮춘다.&lt;/li&gt;
&lt;li&gt;관심사를 분리한다.&lt;/li&gt;
&lt;li&gt;관심사를 모듈로 나눈다.&lt;/li&gt;
&lt;li&gt;클래스/메소드 크기를 줄인다.&lt;/li&gt;
&lt;li&gt;변수/메소드의 이름을 더 나은 것으로 교체한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;중복 제거하기&lt;/h2&gt;
&lt;p&gt;중복은 SW 설계에 있어 가장 큰 적이다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public void scaleToOneDimension(args...) {
    ... // 생략

    Image newImage = ImageUtils.getScaledImage(image, scalingFactor, scalingFactor);
    image.dispose();
    System.gc();
    image = newImage;
}

public synchronized void rotate(int degrees) {
    Image newImage = ImageUtils.getScaledImage(image, degrees);
    image.dispose();
    System.gc();
    image = newImage;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드를 작성하고 보니 일부가 중복이다.&lt;/p&gt;
&lt;p&gt;정리하여 중복을 제거한다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public void scaleToOneDimension(args...) {
    ... // 생략

    replaceImage(ImageUtils.getScaledImage(image, scalingFactor, scalingFactor));
}

public synchronized void rotate(int degrees) {
    replaceImage(ImageUtils.getScaledImage(image, degrees));
}

private void replaceImage(Image newImage) {
    image.dispose();
    System.gc();
    image = newImage;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중복을 제거했더니 조금 보기 좋아졌다.&lt;/p&gt;
&lt;p&gt;하지만 이 클래스를 수정해야 할 이유가 이미지 스케일링, 회전에 이미지 대체까지 생겨버리며 단일 책임 원칙을 제대로 위반해버렸다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;replaceImage&lt;/b&gt; 메소드를 옮겨보자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class ImageReplacer {

    private Image image;
    public ImageReplacer() {}

    public static ImageReplacer builder() {
        return new ImageReplacer();
    }

    public ImageReplacer image(Image image) {
        this.image = image;
        return this;
    }

    public Image replace() {
        return image;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public void scaleToOneDimension(args...) {
    ... // 생략

    image = ImageReplacer.builder()
                .image(ImageUtils.getScaledImage(image, scalingFactor, scalingFactor))
                .replace();
}

public synchronized void rotate(int degrees) {
    image = ImageReplacer.builder()
            .image(ImageUtils.getScaledImage(image, degrees))
            .replace();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ImageReplacer&lt;/code&gt;는 Builder 패턴을 유사하게 사용하여 image를 반환한다.&lt;/p&gt;
&lt;p&gt;이제 이미지 변환의 책임은 &lt;code&gt;ImageReplacer&lt;/code&gt;이 갖게 된다.&lt;/p&gt;
&lt;h3&gt;Template Method 패턴&lt;/h3&gt;
&lt;p&gt;&lt;b&gt;Template Method 패턴&lt;/b&gt;은 중복 제거에 자주 사용되는 기법이다.&lt;/p&gt;
&lt;p&gt;예제를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public class SalaryPolicy {

    public Salary calculateDeveloperSalary() {
        // 대충 경력을 반영한다는 내용
        // 대충 개발한 프로그램의 산출물 평가를 반영한다는 내용
        // 대충 팀에서의 기여도를 수치화한다는 내용
    }

    public Salary calculateDesignerSalary() {
        // 대충 경력을 반영한다는 내용
        // 대충 디자인 산출물을 평가한다는 내용
        // 대충 팀에서의 기여도를 수치화한다는 내용
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 두 메소드는 개발 프로그램의 성과/디자인 만족도 평가 로직을 제외하면 동일하다.&lt;/p&gt;
&lt;p&gt;자신이 맡은 역할에 따라 업무 평가 시스템이 약간 다를 뿐이다.&lt;/p&gt;
&lt;p&gt;여기에 &lt;b&gt;Template Method 패턴&lt;/b&gt;을 적용시켜보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class SalaryPolicy {

    public Salary calculateSalary() {
        reflectLongCareer();
        reflectOutputEvaluation();
        reflectContribution();
    }

    private void reflectLongCareer() { ... }
    abstract protected void reflectOutputEvaluation();
    private void reflectContribution() { ... }
}

public class DeveloperSalaryPolicy extends SalaryPolicy {
    @Override
    public Salary reflectOutputEvaluation() { ... }
}

public class DesignerSalaryPolicy extends SalaryPolicy {
    @Override
    public Salary reflectOutputEvaluation() { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;상위 클래스의 abstract 메소드인 &lt;b&gt;reflectOutputEvaluation&lt;/b&gt;만 채워 급여 체계를 완성했다.&lt;/p&gt;
&lt;p&gt;그 외의 메소드는 상위 클래스에 의존적이다.&lt;/p&gt;
&lt;h2&gt;개발자의 의도를 표현하기&lt;/h2&gt;
&lt;p&gt;본인만 이해하는 코드는 짜기 쉽다.&lt;/p&gt;
&lt;p&gt;다 같이 이해하는 좋은 코드를 짜는 몇 가지 규칙이 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;이해하기 쉬운 이름을 붙이자.&lt;/li&gt;
&lt;li&gt;클래스와 메소드 크기를 가능한 줄인다.&lt;/li&gt;
&lt;li&gt;업계에서 일반적으로 사용되는 명칭을 활용한다. Decorator 패턴을 활용한 경우 *Decorator로 붙인다거나 Queue인 경우 *Queue로 짓는 식이다.&lt;/li&gt;
&lt;li&gt;단위 테스트 케이스를 꼼꼼히 작성한다. 테스트 케이스는 예제를 통해 코드를 검증하는 절차이다. 잘 만든 테스트 케이스는 클래스의 기능을 짐작하기 쉽게 만든다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;무엇보다 가장 중요한 것은 &lt;b&gt;표현하고자 하는 노력&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;나중에 읽을 사람을 위해 약간의 시간을 투자하자.&lt;/p&gt;
&lt;p&gt;그 사람이 내가 될 수도 있다.&lt;/p&gt;
&lt;h2&gt;클래스와 메소드 수를 최소로 줄이기&lt;/h2&gt;
&lt;p&gt;정도를 알아라, 적당히 해라 같은 말이 있다.&lt;/p&gt;
&lt;p&gt;무엇이든 끝까지 파헤치려는 노력은 보기 좋지만, 그 하나에만 매달리며 다른 것을 등한시한다면 득보다 실이 많을 수도 있다.&lt;/p&gt;
&lt;p&gt;SW 설계도 마찬가지다.&lt;/p&gt;
&lt;p&gt;중복을 제거하고, 의도를 잘 표현하고, 단일 책임 원칙을 잘 준수하면 당연히 좋지만,&lt;/p&gt;
&lt;p&gt;정도를 넘어서서 클래수 수와 메소드 수가 셀 수 없을 만큼 많아지면 오히려 관리가 어려울 수도 있다.&lt;/p&gt;
&lt;p&gt;그래서 저자는 '무조건 해라!'가 아니라 '가급적 최소로' 할 것을 권장한다.&lt;/p&gt;
&lt;p&gt;클래스와 메소드 수를 줄이는 &lt;b&gt;궁극적인 목적&lt;/b&gt;은 결국 효율적인 시스템을 개발하기 위해서라는 것을 새겨두자.&lt;/p&gt;</description>
      <category>IT 도서/Clean Code</category>
      <category>Clean Code</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/82</guid>
      <comments>https://private-space.tistory.com/82#entry82comment</comments>
      <pubDate>Tue, 18 Feb 2020 01:15:31 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] EntityManager, 영속성 컨텍스트</title>
      <link>https://private-space.tistory.com/81</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;김영한 님의 [자바 ORM 표준 JPA 프로그래밍]을 읽고 내용을 정리한 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788960777330&amp;amp;orderClick=LEa&amp;amp;Kc=&quot;&gt;교보문고 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;EntityManager와 영속성 컨텍스트&lt;/h1&gt;
&lt;p&gt;mybatis처럼 직접 SQL을 작성하거나 JPA를 활용한 개발을 할 때는 눈에 보이는 것 이상의 차이가 있다.&lt;/p&gt;
&lt;p&gt;가장 사소한 예시를 들어보자면 CRUD로 불리는 가장 기본적인 4가지 동작 중 UPDATE는 JPA에 존재하지 않는다.&lt;/p&gt;
&lt;p&gt;그럼에도 JPA는 업데이트 동작을 잘 수행한다.&lt;/p&gt;
&lt;p&gt;영속성 컨텍스트가 데이터의 변경을 감지하여 자동으로 update 쿼리를 실행하기 때문이다.&lt;/p&gt;
&lt;p&gt;JPA는 &lt;code&gt;EntityManager&lt;/code&gt;와 &lt;code&gt;영속성 컨텍스트&lt;/code&gt;를 통해 데이터의 상태 변화를 감지하고 필요한 쿼리를 자동으로 수행한다.&lt;/p&gt;
&lt;p&gt;Spring boot + Spring Data를 사용하여 막 JPA를 배우는 상황이면 &lt;code&gt;EntityManager&lt;/code&gt;를 한 번도 못 봤을 수 있는데,&lt;/p&gt;
&lt;p&gt;Application이 시작될 때 &lt;code&gt;EntityManager&lt;/code&gt;를 자동으로 bean에 등록하고 우리가 알지 못하는 사이에 가져다 사용하고 있다.&lt;/p&gt;
&lt;h2&gt;Entity Manager 생성&lt;/h2&gt;
&lt;pre class=&quot;protobuf&quot;&gt;&lt;code&gt;// name에 persistance unit name을 등록할 수 있다.
EntityManagerFactory emf = Persistance.createEntityManagerFactory(&quot;name&quot;);
EntityManager em = emf.createEntityManager();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;EntityManager&lt;/code&gt;를 생성할 땐 가급적 팩토리를 활용한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EntityManagerFactory&lt;/code&gt;는 Thread safe한 처리가 되어 있으나, &lt;code&gt;EntityManager&lt;/code&gt;는 그렇지 않기 때문에 스레드 간 공유에 주의해야 한다.&lt;/p&gt;
&lt;h2&gt;영속성 컨텍스트 (Persistence Context)&lt;/h2&gt;
&lt;p&gt;엔티티를 영구 저장하는 환경이다.&lt;/p&gt;
&lt;p&gt;Java 영역에서 데이터를 관리하여 DB 접근을 최적화하는 역할을 담당한다.&lt;/p&gt;
&lt;h3&gt;생명 주기&lt;/h3&gt;
&lt;p&gt;엔티티는 4가지 상태가 존재한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;비영속&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;영속성 컨텍스트와 연관이 없는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;영속&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;영속성 컨텍스트에서 관리 중인 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;준영속&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;영속성 컨텍스트에 저장되어 있었으나 분리된 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;삭제&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;영속성 컨텍스트에서 완전히 삭제된 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;비영속&lt;/h4&gt;
&lt;p&gt;@Entity 어노테이션을 갖는 엔티티 인스턴스를 막 생성했을 때는 영속성 컨텍스트에서 관리하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EntityManager&lt;/code&gt;의 persist 메소드를 사용하여 영속 상태로 변경할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;em.persist(someEntity);&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;영속&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;EntityManager&lt;/code&gt;를 통해 데이터를 영속성 컨텍스트에 저장했다.&lt;/p&gt;
&lt;p&gt;JPA는 일반적으로 id 필드가 존재하지 않으면 예외를 뱉어내는데, 영속 상태의 엔티티를 관리하기 위해서다.&lt;/p&gt;
&lt;p&gt;id로 데이터를 관리하기 때문에 꼭 필요한 것이다.&lt;/p&gt;
&lt;p&gt;이 상태가 되면 몇 가지의 장점을 갖게 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;1차 캐시&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;em.find(key)&lt;/code&gt;를 호출하면 영속성 컨텍스트에 캐시된 데이터를 먼저 찾는다.&lt;/li&gt;
&lt;li&gt;캐시된 데이터가 없다면 DB에 접근하여 데이터를 로드하고 1차 캐시 데이터에 저장한다.&lt;/li&gt;
&lt;li&gt;1차 캐시의 존재로 Java 영역에서 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 등급의 트랜잭션 격리 수준을 활용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;동일성 보장&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JPA를 통해 불러온 데이터는 모두 캐시 데이터에 저장되기 때문에 같은 id를 가진 데이터는 같은 데이터이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일반적으로 Java에서 '같다'라는 기준은 Identity(hashcode) / Equality(equals)이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;SomeEntity a = em.find(SomeEntity.class, &quot;1&quot;);
SomeEntity b = em.find(SomeEntity.class, &quot;1&quot;);
// a == b : true (Identity)
// a.equals(b) : true (Equality)&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;트랜잭션 지원하는 쓰기 지연&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transaction이 시작된 이후 JPA가 생성한 쿼리는 모두 쓰기 지연 저장소에 저장된다.&lt;/li&gt;
&lt;li&gt;commit이 수행되면 저장된 모든 쿼리를 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;변경 감지&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL을 직접 활용하여 개발하면 update문을 수행할 때 매우 귀찮은 점이 있다.&lt;/li&gt;
&lt;li&gt;컬럼 1개, 2개, 3개, ... N개를 수정해야 할 때를 모두 쿼리로 작성해야 하는 것이다.&lt;/li&gt;
&lt;li&gt;이렇게 되면 비즈니스 로직은 SQL에 의존할 수밖에 없다.&lt;/li&gt;
&lt;li&gt;JPA는 데이터를 저장하기 전 영속성 컨텍스트에 저장된 데이터가 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;동일 데이터가 존재하면 update, 없으면 insert를 수행한다(upsert).&lt;/li&gt;
&lt;li&gt;JPA가 실제로 수행하는 쿼리는 모든 컬럼을 변경한다.
&lt;ul&gt;
&lt;li&gt;컬럼이 굉장히 많은(30개 이상) 테이블이 아니면 성능에 크게 영향을 미치지 않는다.&lt;/li&gt;
&lt;li&gt;엔티티 클래스에 &lt;code&gt;@DynamicUpdate&lt;/code&gt;를 붙여주면 SET절에 &lt;b&gt;변경된 데이터&lt;/b&gt;만 삽입된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;지연 로딩&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;준영속&lt;/h4&gt;
&lt;p&gt;원래 영속 상태였으나 영속성 컨텍스트에서 분리되어 더 이상 관리하지 않는 데이터가 된 상태이다.&lt;/p&gt;
&lt;p&gt;영속 상태의 엔티티를 detach 시키거나 영속성 컨텍스트 자체가 초기화 / 종료되면 컨텍스트 내부의 모든 데이터는 준영속 상태가 된다.&lt;/p&gt;
&lt;p&gt;관리되지는 않는 상태이지만 JPA의 지원을 받지 못할 뿐, 정상적인 데이터를 갖는 인스턴스이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 1.
em.detach(someEntity);
// 2.
em.close();
// 3.
em.clear();&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;삭제&lt;/h4&gt;
&lt;p&gt;엔티티를 영속성 컨텍스트와 DB 양쪽에서 모두 삭제한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;em.remove(someEntity);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;플러시 (flush)&lt;/h3&gt;
&lt;p&gt;영속성 컨텍스트의 변경 내용을 DB에 반영하는 절차이다.&lt;/p&gt;
&lt;p&gt;플러시를 수행하면 아래 순서대로 동작한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;데이터의 변경을 감지한다.&lt;/li&gt;
&lt;li&gt;생성된 쿼리를 쓰기 지연 저장소에 등록한다.&lt;/li&gt;
&lt;li&gt;commit되면 저장되어 있던 쿼리를 모두 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;em.flush()&lt;/code&gt;를 활용하면 직접 플러시할 수 있다.&lt;/p&gt;
&lt;h4&gt;플러시 모드&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@FlushModeType.AUTO&lt;/code&gt; (default)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;commit이나 쿼리 실행할 때 플러시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@FlushModeType.COMMIT&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;commit할 때만 플러시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;종료&lt;/h3&gt;
&lt;p&gt;영속성 컨텍스트를 종료하려면 &lt;code&gt;EntityManager&lt;/code&gt;의 close 메소드를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;em.close();&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;병합&lt;/h3&gt;
&lt;p&gt;준영속 상태의 데이터는 병합 기능을 사용하여 다시 영속 상태로 돌릴 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;SomeEntity entity = em.find(key);
em.detach(entity);
em.merge(entity);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Java/JPA</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/81</guid>
      <comments>https://private-space.tistory.com/81#entry81comment</comments>
      <pubDate>Mon, 17 Feb 2020 00:17:47 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Code] 시스템</title>
      <link>https://private-space.tistory.com/80</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;Clean Code를 읽고 내용을 정리한 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788966260959&amp;amp;orderClick=LEA&amp;amp;Kc=&quot;&gt;책 구매하러 가기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;시스템&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;By &lt;strong&gt;Ray Ozzie&lt;/strong&gt; (Microsoft CTO)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;도시&lt;/h2&gt;
&lt;p&gt;현대의 도시는 온갖 세세한 사항들을 나누어서 맡는 식으로 관리되고 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;수도 관리 (수도공사)&lt;/li&gt;
&lt;li&gt;치안 관리 (경찰)&lt;/li&gt;
&lt;li&gt;재난 (소방서)&lt;/li&gt;
&lt;li&gt;전기 관리 (한전)&lt;/li&gt;
&lt;li&gt;... etc&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;도시를 소스코드로 작성한다고 하면 도시를 구성하는 세부 사항들이 추상화/모듈화된 것으로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;큰 그림을 이해하지 못해도 작은 단위의 기능들은 잘 동작하기 때문에 효율적인 것이다.&lt;/p&gt;
&lt;h2&gt;관심사 분리&lt;/h2&gt;
&lt;p&gt;살펴보고자 하는 관심사는 &lt;strong&gt;시작 단계&lt;/strong&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    public static Singleton instance = new Singleton();
    private Singleton() {}
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 코드는 가장 간단하게 작성할 수 있는 싱글턴 패턴의 코드이다.&lt;/p&gt;
&lt;p&gt;어플리케이션이 시작할 때 인스턴스가 생성될 것이다.&lt;/p&gt;
&lt;p&gt;위와 비슷한 유형의 코드가 많아지면 시작 단계에서 불필요한 부하가 생긴다.&lt;/p&gt;
&lt;p&gt;심지어 &lt;code&gt;Singleton&lt;/code&gt; 클래스가 실제로 사용되지 않을 수도 있다.&lt;/p&gt;
&lt;p&gt;이는 불필요한 자원 낭비로 이어진다.&lt;/p&gt;
&lt;h3&gt;Refactoring&lt;/h3&gt;
&lt;p&gt;필요할 때 생성하는 방식으로 바꿔본다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private static Singleton instance;
    private Singleton Singleton() {}
    public getInstance() {
        if (instance == null) instance = new Singleton();
        return instance;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위의 코드는 실제로 Singleton이 필요해지기 전까지 인스턴스를 생성하지 않는다.&lt;/p&gt;
&lt;p&gt;이런 기법을 초기화 지연(Lazy Initialization)이라고 부른다.&lt;/p&gt;
&lt;p&gt;이 코드에도 다중 스레드 시스템에서 안정성을 보장하지 못하는 문제가 있지만, 싱글톤에 대해 논의하는 글이 아니기 때문에 넘어가도록 한다.&lt;/p&gt;
&lt;h3&gt;의존성 문제&lt;/h3&gt;
&lt;p&gt;이번엔 다른 예시를 살펴보겠다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Service getService() {
    if (service == null) service = new ServiceImpl(...); // 매개변수 생략
    return servce;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;lazy 기법을 사용해서 실제로 필요한 순간 인스턴스를 생성하고 반환하는 코드이다.&lt;/p&gt;
&lt;p&gt;이 코드는 &lt;code&gt;ServiceImpl&lt;/code&gt;에 명시적으로 의존하고 있다.&lt;/p&gt;
&lt;p&gt;또 &lt;code&gt;ServiceImpl&lt;/code&gt;을 생성할 때 사용하는 매개변수가 정해져 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ServiceImpl&lt;/code&gt;이라는 구현체는 어느 상황에서나 필요하다고 확신할 수 있을까?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;클래스의 생성과 사용에 대한 관심사 분리&lt;/strong&gt;가 되어 있지 않기 때문에 발생하는 상황이다.&lt;/p&gt;
&lt;h3&gt;Main 분리&lt;/h3&gt;
&lt;p&gt;실제로 생성하는 부분과 가져다 사용하는 부분을 모듈화하여 분리한다고 하자.&lt;/p&gt;
&lt;p&gt;사용하는 곳에서 보면 인스턴스가 생성되는 과정을 전혀 알 수 없고 적절한 방법으로 생성되었겠거니 하는 짐작만 가능한 시스템을 제작한다.&lt;/p&gt;
&lt;p&gt;그렇게 되면 조금 더 효율적으로 인스턴스를 관리할 수 있다.&lt;/p&gt;
&lt;p&gt;실제로 이러한 기능을 제공하는 프레임워크가 다수 존재한다.&lt;/p&gt;
&lt;p&gt;자바 진영에선 &lt;strong&gt;Spring Framework&lt;/strong&gt;가 대표적이다.&lt;/p&gt;
&lt;h4&gt;의존성 주입&lt;/h4&gt;
&lt;p&gt;Spring Framework의 핵심 요소 중 하나는 &lt;strong&gt;의존성 주입 (Dependency Injection)&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;의존성 주입은 &lt;strong&gt;제어의 역전 (Inversion Of Control)&lt;/strong&gt; 기법으로 의존성을 관리하는 방법을 말한다.&lt;/p&gt;
&lt;p&gt;제어의 역전은 본래 개발자가 가질 책임인 제어의 권한을 Spring framework에 넘겨 코드에만 집중할 수 있는 환경을 마련한다.&lt;/p&gt;
&lt;h2&gt;도시 확장하기&lt;/h2&gt;
&lt;p&gt;작은 도시가 큰 도시로 확장되어 가는 과정은 큰 고난이 따르기 마련이다.&lt;/p&gt;
&lt;p&gt;조그마한 마을이 갑자기 성장하여 큰 도시가 되어 버렸을 때, 어느 골목의 교통량 또한 크게 늘어 기존의 2차선으로 부족한 상황이 생겼다고 하자.&lt;/p&gt;
&lt;p&gt;왜 처음부터 6 ~ 8차선 이상의 도로로 만들지 않았을까 하는 고민은 의미가 없다.&lt;/p&gt;
&lt;p&gt;작은 마을엔 4차선 이상의 도로도 낭비이기 때문이라는 것을 잘 알고 있기 때문이다.&lt;/p&gt;
&lt;p&gt;SW도 마찬가지로 처음부터 깨끗하고 올바른 아키텍처를 만드는 것은 쉽지 않다.&lt;/p&gt;
&lt;p&gt;오늘 할 일에 집중하고 내일의 일을 예상하여 시스템을 적절하게 확장시킬 여지를 만들어가는 것이 중요하다.&lt;/p&gt;
&lt;h2&gt;AOP&lt;/h2&gt;
&lt;p&gt;관심사를 분리하는 기법 중 하나이다.&lt;/p&gt;
&lt;p&gt;AOP는 횡단 관심사를 찾는다.&lt;/p&gt;
&lt;p&gt;서로 다른 클래스에 선언된 메소드에서 특정 기능(로깅, 트랜잭션 처리 등)을 반복적으로 사용한다면,&lt;/p&gt;
&lt;p&gt;로깅이나 트랜잭션 처리는 횡단 관심사로 처리하는 것이다.&lt;/p&gt;
&lt;p&gt;Spring framework에서는 프록시 패턴으로 AOP를 처리하고 있다.&lt;/p&gt;</description>
      <category>IT 도서/Clean Code</category>
      <category>Clean Code</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/80</guid>
      <comments>https://private-space.tistory.com/80#entry80comment</comments>
      <pubDate>Sun, 16 Feb 2020 18:49:59 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Code] 클래스</title>
      <link>https://private-space.tistory.com/79</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;Clean Code를 읽고 내용을 정리한 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788966260959&amp;amp;orderClick=LEA&amp;amp;Kc=&quot;&gt;책 구매하러 가기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;클래스&lt;/h1&gt;
&lt;h2&gt;클래스 구성&lt;/h2&gt;
&lt;p&gt;자바 표준은 클래스를 아래의 순서대로 구성하라고 권고하고 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;static public 상수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;static private 변수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;private 변수&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자바에서 비정적 public 변수가 필요한 일은 거의 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;public method&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;private method는 호출하는 public method 함수 뒤에 넣는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;추상화 단계를 순차적으로 내려가도록 지키면 신문 기사처럼 가독성이 좋은 코드를 작성할 수 있다.&lt;/p&gt;
&lt;h3&gt;클래스의 크기&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;클래스는 작아야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;메소드는 줄 수로 크기를 측정하지만 클래스는 맡은 책임의 개수로 크기를 측정한다.&lt;/p&gt;
&lt;p&gt;클래스의 책임에 관련된 이야기는 &lt;strong&gt;[OOP 5대 원칙]&lt;/strong&gt; 관련 내용을 찾아보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SuperDashboard extends JFrame implements MetaDataUser {
    public String getCustomizerLanguagePath() {...}
    public void setSystemConfigPath(String systemConfigPath) {...}
    public String getSystemConfigDocument() {...}
    public void setSystemConfigDocument(String systemConfigDocument) {...}
    public boolean getGuruState() {...}
    public boolean getNoviceState() {...}
    ... // 이외에도 수많은 메소드가 포함되어 있음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SuperDashboard&lt;/code&gt;는 맡은 책임이 너무 많다.&lt;/p&gt;
&lt;p&gt;클래스 이름은 클래스가 맡은 책임을 함축적으로 요약할 수 있어야 한다.&lt;/p&gt;
&lt;p&gt;간결한 이름이 떠오르지 않는다면 클래스의 크기가 너무 커서 그럴 가능성이 높다.&lt;/p&gt;
&lt;p&gt;위의 &lt;code&gt;SuperDashboard&lt;/code&gt;를 적절한 이름으로 교체하기는 매우 어려워보인다.&lt;/p&gt;
&lt;h3&gt;단일 책임 원칙&lt;/h3&gt;
&lt;p&gt;위에서 언급한 OOP 5대 원칙 중 하나이다.&lt;/p&gt;
&lt;p&gt;클래스는 단 하나의 책임만을 가져야 한다는 것이다.&lt;/p&gt;
&lt;p&gt;아래는 &lt;code&gt;SuperDashboard&lt;/code&gt;에서 몇 개를 골라 다른 클래스로 옮겨낸 예제이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Version {
    public int getMajorVersion() {}
    public int getMinorVersion() {}
    public int getBuildVersion() {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Version&lt;/code&gt;이라는 이름이 한층 더 쉽게 와닿으며 다른 코드에서도 재사용하기 쉬워보인다.&lt;/p&gt;
&lt;h4&gt;단일 책임 원칙이 지켜지지 않는 이유&lt;/h4&gt;
&lt;p&gt;사람의 두뇌 용량은 한계가 있다보니 관심사를 분리하고 개발에 임할 수밖에 없다.&lt;/p&gt;
&lt;p&gt;그리고 대부분은 &amp;#39;깨끗하고 보기 좋은 코드&amp;#39;보다 &amp;#39;잘 작동하는 코드&amp;#39;에 초점을 맞출 수밖에 없다.&lt;/p&gt;
&lt;p&gt;여기까지는 당연한 이야기지만 문제는 &amp;#39;잘 작동하는 코드&amp;#39;를 작성한 뒤 관련 업무를 끝내버리는 것이다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;단일 책임 클래스가 많아지면 소스 코드를 이해하기 어렵지 않을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;어짜피 어플리케이션의 규모가 커지면 클래스는 많아지기 마련이다.&lt;/p&gt;
&lt;p&gt;우리가 더 집중해야 하는 것은 어떻게 정리해야 쉽게 관리할 수 있느냐이다.&lt;/p&gt;
&lt;p&gt;일과를 마치고 옷을 대충 땅바닥에 던져 두면 빠르게 찾을 수는 있지만 원하는 옷을 빠르게 찾기는 어려울 수 있다.&lt;/p&gt;
&lt;p&gt;여러 개의 옷장을 두고 각 옷장에 어떤 옷을 넣어둘 지 규칙을 정하면 원하는 옷을 빠르게 찾아낼 수 있을 것이다.&lt;/p&gt;
&lt;h4&gt;응집도&lt;/h4&gt;
&lt;p&gt;클래스는 인스턴스 변수를 적게 가지는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;변경하기 쉬운 클래스&lt;/h2&gt;
&lt;p&gt;서비스가 지속되는 한 클래스는 계속 바뀔 수밖에 없다.&lt;/p&gt;
&lt;p&gt;무언가를 변경할 때마다 시스템이 의도처럼 동작하지 않을 가능성이 있다.&lt;/p&gt;
&lt;p&gt;이를 해결하는 것은 체계적이고 깨끗한 코드를 작성하여 변경의 위험을 낮추는 것이다.&lt;/p&gt;
&lt;p&gt;아래의 클래스는 DB에 접근하는 &lt;code&gt;Sql&lt;/code&gt; 클래스이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Sql {
    public Sql(String table, Column[] columns)
    public String create()
    public String insert(Object[] fields)
    public String selectAll()
    public String select(Column column, String pattern)
    public String select(Criteria criteria)
    public String preparedInsert()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Sql&lt;/code&gt; 클래스는 단일 책임 원칙이 지켜지고 있지 않다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Sql&lt;/code&gt; 클래스의 내용을 보면 update, delete를 처리하는 메소드가 존재하지 않는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;update, delete를 처리하기 위해서는 &lt;code&gt;Sql&lt;/code&gt; 클래스를 수정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;select문에서 더 복잡한 쿼리를 지원해야 하는 상황이 생겼다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;select 메소드를 수정하기 위해 &lt;code&gt;Sql&lt;/code&gt; 클래스를 수정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;간단한 예시임에도 수정해야 할 이유가 2가지나 있다.&lt;/p&gt;
&lt;h3&gt;Refactoring&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Sql&lt;/code&gt; 클래스를 리팩토링하여 책임을 분리해보자.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Sql&lt;/code&gt;을 추상화한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public abstract class Sql {
    public Sql(String table, Column[] columns)
    abstract public String generate();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;code&gt;Sql&lt;/code&gt;을 상속받는 자식 클래스를 생성한다. 자식 클래스는 실제 쿼리 구현을 담당한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 쿼리 생성
public class CreateSql extends Sql {
    public CreateSql(String table, Column[] columns)
    @Override public String generate()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SelectSql extends Sql {
    public SelectSql(String table, Column[] columns)
    @Override public String generate()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class InsertSql extends Sql {
    public InsertSql(String table, Column[] columns)
    @Override public String generate()
    private String valuesList(Object[] fields, final Column[] columns)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class PreparedInsertSql extends Sql {
    public PreparedInsertSql(String table, Column[] columns)
    @Override public String generate()
    private String placeHolderList(Column[] columns)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Where {
    public Where(String criteria)
    public String generate()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드에서 update/delete를 추가하고 싶다면 &lt;code&gt;Sql&lt;/code&gt;을 상속받아서 구현하면 된다.&lt;/p&gt;
&lt;p&gt;단일 책임 원칙을 충실히 따르고 있으며 개방 폐쇄 원칙(Open-Close Principle)까지 준수하고 있다.&lt;/p&gt;
&lt;p&gt;새 기능 추가 및 기존 기능을 변경하려고 한다면 위의 코드처럼 기존 코드를 변경할 가능성이 최소인 구성이 좋다.&lt;/p&gt;</description>
      <category>IT 도서/Clean Code</category>
      <category>Clean Code</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/79</guid>
      <comments>https://private-space.tistory.com/79#entry79comment</comments>
      <pubDate>Sun, 16 Feb 2020 16:47:28 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Code] 단위 테스트</title>
      <link>https://private-space.tistory.com/78</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;Clean Code를 읽고 내용을 정리한 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788966260959&amp;amp;orderClick=LEA&amp;amp;Kc=&quot;&gt;책 구매하러 가기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;단위 테스트&lt;/h1&gt;
&lt;h2&gt;TDD의 법칙&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.&lt;/li&gt;
&lt;li&gt;컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.&lt;/li&gt;
&lt;li&gt;현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위의 규칙을 따르면 매일 굉장히 많은 테스트 코드가 산출물로 작성될 것이다.&lt;/p&gt;
&lt;p&gt;SW 오류를 조금 더 정확하게 짚어낼 수 있겠지만 방대한 테스트 코드는 관리 문제를 유발하기도 한다.&lt;/p&gt;
&lt;h3&gt;깨끗한 테스트 코드 유지하기&lt;/h3&gt;
&lt;p&gt;지저분한 테스트 코드는 없는 것보다 못할 수도 있다.&lt;/p&gt;
&lt;p&gt;테스트 코드가 굉장히 많이 작성되어 있는데 SW의 스펙이 변경된다면 실제 코드도 변경해야 한다.&lt;/p&gt;
&lt;p&gt;실제 코드가 변경되면 테스트 코드는 반드시 같이 변경되어야 한다.&lt;/p&gt;
&lt;p&gt;또 테스트 코드가 너무 많다면 실제 코드보다 테스트 케이스를 추가하는 시간이 더 걸릴 수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;테스트 코드는 실제 코드를 작성하듯 신중하고 깨끗하게 작성해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;테스트 코드는 유연성, 유지보수성, 재사용성을 제공한다&lt;/h3&gt;
&lt;p&gt;테스트 코드가 있으면 코드 변경이 두렵지 않다.&lt;/p&gt;
&lt;p&gt;내가 틀렸다면 알아서 검증해주기 때문이다.&lt;/p&gt;
&lt;p&gt;테스트 케이스가 없으면 모든 변경이 잠정적인 버그이다.&lt;/p&gt;
&lt;p&gt;테스트 코드가 지저분하면 코드를 변경하는 능력 또한 떨어진다.&lt;/p&gt;
&lt;h3&gt;깨끗한 테스트 코드&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;가독성&lt;/li&gt;
&lt;li&gt;가독성&lt;/li&gt;
&lt;li&gt;가독성&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;가독성은 몇 번을 강조해도 부족하다.&lt;/p&gt;
&lt;p&gt;테스트 코드는 최소의 표현으로 명료하게 나타내야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void testGetPageHierarchyAsXML() throws Exception {
    makePages(&amp;quot;pageOne&amp;quot;, &amp;quot;pageOne.childOne&amp;quot;, &amp;quot;pageTwo&amp;quot;);
    submitRequest(&amp;quot;root&amp;quot;, &amp;quot;type:pages&amp;quot;);

    assertResponseIsXML();
    asserResponseContains(&amp;quot;&amp;lt;name&amp;gt;pageOne&amp;lt;/name&amp;gt;&amp;quot;,
        &amp;quot;&amp;lt;name&amp;gt;pageTwo&amp;lt;/name&amp;quot;, &amp;quot;&amp;lt;name&amp;gt;childOne&amp;lt;/name&amp;quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드를 보면 makePages를 정확히 명세하지 않았지만 어떤 역할을 할 것인지 짐작하기 쉬울 것이다.&lt;/p&gt;
&lt;h3&gt;하나의 개념에선 하나만 테스트&lt;/h3&gt;
&lt;p&gt;몇 개의 개념을 하나의 테스트에서 같이 진행하게 되면 코드를 읽는 사람이 그 모든 개념을 한 번에 이해해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void testAddMonths() {
    SerialDate d1 = SerialDate.createInstance(2004, 5, 31);

    SerialDate d2 = SerialDate.addMonths(1, d1);
    asserEquals(2004, d2.getYYYY());
    asserEquals(6, d2.getMonth());
    asserEquals(30, d2.getDayOfMonth());

    SerialDate d3 = SerialDate.addMonths(2, d1);
    asserEquals(2004, d3.getYYYY());
    asserEquals(7, d3.getMonth());
    asserEquals(31, d3.getDayOfMonth());

    SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1);
    asserEquals(2004, d4.getYYYY());
    asserEquals(7, d4.getDayOfMonth());
    asserEquals(30, d4.getMonth());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드를 해석해보자.&lt;/p&gt;
&lt;p&gt;일단, 메소드명으로 짐작컨대 날짜를 월 단위로 계산하는 테스트 코드로 보인다.&lt;/p&gt;
&lt;p&gt;5월 31일에서 한 달을 더하면 6월 30일이 된다.&lt;/p&gt;
&lt;p&gt;5월 31일에서 두 달을 더하면 7월 31일이 된다.&lt;/p&gt;
&lt;p&gt;5월의 마지막 날에서 더했기 때문에 6월 마지막, 7월 마지막으로 설정된 것이다.&lt;/p&gt;
&lt;p&gt;d4는 5월 31일에서 1달을 더하고(6/30), 거기에 1달을 더 더한다.&lt;/p&gt;
&lt;p&gt;그래서 7월 30일이 나왔다.&lt;/p&gt;
&lt;p&gt;이 코드에서는 여러 개념이 한 번에 등장했다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;addMonths는 일 단위가 아니라 월 단위로만 계산한다.&lt;/li&gt;
&lt;li&gt;말일에서 1달을 더하면 다음 달 말일이 된다.&lt;/li&gt;
&lt;li&gt;최종 값은 중간 값의 결과에 따라 달라질 수 있다(d4).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;개념을 최소한으로 줄이면 한 단위의 코드에서 이해해야 하는 양을 줄일 수 있다.&lt;/p&gt;
&lt;h3&gt;FIRST 법칙&lt;/h3&gt;
&lt;p&gt;깨끗한 테스트는 FIRST 규칙을 따른다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Fast&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테스트는 빨라야 한다.&lt;/li&gt;
&lt;li&gt;느린 테스트는 돌릴 때마다 두렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Independent&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 테스트는 독립적이어야 한다.&lt;/li&gt;
&lt;li&gt;한 테스트가 다음 테스트의 환경을 준비해선 안 된다.&lt;/li&gt;
&lt;li&gt;의존성이 생기면 테스트가 연달아 실패한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Repeatable    &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테스트는 어떤 환경에서도 반복 가능해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Self-Validation    &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테스트는 성공/실패 중 하나의 값만 가져야 한다.&lt;/li&gt;
&lt;li&gt;테스트가 스스로 성공/실패를 가늠하지 못하면 주관적인 판단이 개입될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Timely    &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테스트는 적시에 작성해야 한다.&lt;/li&gt;
&lt;li&gt;단위 테스트는 테스트하려는 실제 코드를 구현하기 전에 먼저 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>IT 도서/Clean Code</category>
      <category>Clean Code</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/78</guid>
      <comments>https://private-space.tistory.com/78#entry78comment</comments>
      <pubDate>Sat, 15 Feb 2020 03:25:04 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Code] 주석</title>
      <link>https://private-space.tistory.com/77</link>
      <description>&lt;blockquote&gt;
&lt;p&gt;Clean Code를 읽고 내용을 정리한 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788966260959&amp;amp;orderClick=LEA&amp;amp;Kc=&quot;&gt;책 구매하러 가기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;주석&lt;/h1&gt;
&lt;p&gt;코드를 표현하는 대표적인 수단&lt;/p&gt;
&lt;p&gt;너무 과신하진 말자. 주석은 오래될수록 코드에서 멀어지며 전혀 다른 의미를 갖게될 수 있다.&lt;/p&gt;
&lt;h2&gt;주석은 나쁜 코드를 보완하지 못한다.&lt;/h2&gt;
&lt;p&gt;주석을 추가하는 일반적인 이유는 명백하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;코드 품질이 나쁘기 때문&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;주석을 달기보다는 깔끔한 코드를 작성하기 위해 노력하자.&lt;/p&gt;
&lt;h2&gt;코드에 의도를 담아보자&lt;/h2&gt;
&lt;p&gt;대다수의 코드에는 개발자의 의도를 충분히 담을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;
// 1)
if (employee.flags &amp;amp; DEV_FLAG) {
    // Do something...
}

// 2)
if (employee.isDeveloper()) {
    // Do something...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드에서 1과 2 중 어느 코드에 개발자의 의도가 더 잘 담겨 있을까?&lt;/p&gt;
&lt;p&gt;주석 대신 메소드 이름에 의도를 담는 편이 이해하기 쉽다.&lt;/p&gt;
&lt;h2&gt;좋은 주석&lt;/h2&gt;
&lt;h3&gt;주석으로 정보를 제공하자&lt;/h3&gt;
&lt;p&gt;의도를 담기 어려운 코드도 있을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// kk:mm:ss EEE, MMM dd, yyyy 형식
Pattern timeFormat = Pattern.compile(&amp;quot;\\d*:\\d:\\d* \\w*, \\w* \\d* \\d*&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정규표현식은 익숙하지 않으면 한 눈에 살펴보기 어렵다.&lt;/p&gt;
&lt;p&gt;이런 경우 주석 한 줄이 개발자의 시간을 절약한다.&lt;/p&gt;
&lt;h3&gt;의도를 설명하는 주석&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 스레드를 많이 생성하여 시스템에 영향을 끼침
for (int i = 0; i &amp;lt; 25000; i++) {
    SomeThread someThread = ThreadBuilder.builder().build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그냥 보면 왜 스레드를 저렇게 많이 생성하지? 하는 생각이 들 수 있으나&lt;/p&gt;
&lt;p&gt;주석 한 줄로 그 의도를 알 수 있다.&lt;/p&gt;
&lt;h3&gt;의미를 확실하게 밝히는 주석&lt;/h3&gt;
&lt;p&gt;애매한 매개 변수나 반환값을 설명하기 위해 주석을 사용해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;assertTrue(a.compareTo(a) == 0); // a == a
assertTrue(a.compareTo(b) != 0); // a != b
assertTrue(ab.compareTo(ab) == 0); // ab == ab&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;명료하게 의미를 밝히면 코드를 해석하기 더 쉽다.&lt;/p&gt;
&lt;p&gt;하지만 어려운 로직을 설명하기 위한 주석이 달려있어도 주석이 올바른지 검증하기 어려울 수도 있다.&lt;/p&gt;
&lt;p&gt;주석을 달기 전에 더 나은 방법이 없는지 고민해보자.&lt;/p&gt;
&lt;h3&gt;경고 목적의 주석&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 여유 있을 때만 실행하고 그 이외엔 skip test 옵션 사용하세요.
@Test
public void test() {
    for (int i = 0; i &amp;lt; 100000000; i++) {
        // do something...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;TODO, FIXME, XXX&lt;/h3&gt;
&lt;p&gt;특별한 것을 기록하기 위한 주석도 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;TODO&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;당장 구현하기 어려운 일&lt;/li&gt;
&lt;li&gt;추후 해야할 일&lt;/li&gt;
&lt;li&gt;더 좋은 이름으로 바꿔달라는 부탁 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;FIXME&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;문제는 있는데 당장 수정할 필요는 없어서 남김&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;XXX&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;더 생각해볼 필요가 있는 항목&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;셋 중 TODO의 사용 빈도가 제일 높다.&lt;/p&gt;
&lt;p&gt;요샌 IDE에서 TODO의 위치를 다 찾아주니 추후 진행해야 하는 일이 있으면 꼭 TODO를 기록해놓자.&lt;/p&gt;
&lt;p&gt;단, 나쁜 코드를 남겨놓고 너무 어려워서 다음에 하겠다는 핑계의 의미로 TODO는 사용하지 않는다.&lt;/p&gt;
&lt;h3&gt;중요성 강조&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// trim이 없으면 의도와 다르게 동작할 수 있음.
if (StringUtils.isEmpty(userId.trim())) {
    // do something...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드가 사용자의 ID를 검증한다고 가정해보자.&lt;/p&gt;
&lt;p&gt;누군가가 악의적으로 앞뒤에 공백을 넣은 코드를 통과시키려고 할 수 있으나, trim을 사용하여 공백을 제거하였다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 trim이 중요하다는 것을 주석에서 말하고 있다.&lt;/p&gt;
&lt;h2&gt;나쁜 주석&lt;/h2&gt;
&lt;p&gt;마지못해 다는 주석은 잡담이나 다름없다.&lt;/p&gt;
&lt;p&gt;주석을 달아야 한다면, 꼭 필요한 주석만을 남기도록 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;try {
    FileInputStream fis = new FileInputStream(filePath);
    loadProperties.load(fis);
} catch (IOException e) {
    // properties 파일이 없으면 기본값이 모두 메모리에 포함되어 있음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;catch 블록의 주석은 작성자 말고는 이해하기 어려워 보인다.&lt;/p&gt;
&lt;p&gt;properties는 어디에 선언되어 있는가? 왜 읽는가?&lt;/p&gt;
&lt;p&gt;부가적인 정보를 찾기 위해 관련 코드를 모두 뒤져야만 하기 때문이다.&lt;/p&gt;
&lt;p&gt;결국 이 주석은 다른 개발자와 전혀 소통이 되지 않음을 의미한다.&lt;/p&gt;
&lt;h3&gt;중복되는 주석&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Foo {
    /**
     * class manager
     */
    public Manager manager;

    /**
     * class cluster
     */
    public Cluster cluster;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 주석은 과연 영양가가 있는 주석일까?&lt;/p&gt;
&lt;p&gt;같은 말을 두 번 하는 것과 다를 바가 없다.&lt;/p&gt;</description>
      <category>IT 도서/Clean Code</category>
      <category>Clean Code</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/77</guid>
      <comments>https://private-space.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 14 Feb 2020 22:49:56 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Code] 함수</title>
      <link>https://private-space.tistory.com/76</link>
      <description>&lt;blockquote data-ke-size=&quot;size14&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Clean Code를 읽고 내용을 정리한 것입니다.&lt;/span&gt;&lt;br /&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788966260959&amp;amp;orderClick=LEA&amp;amp;Kc=&quot;&gt;책 구매하러 가기&lt;/a&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 작게 만들어라&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;함수는 100줄을 넘기지 마라.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 100줄이 넘어가는 함수가 있다면 따로 함수로 분리하라.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그래야 읽고 이해하기 쉬워진다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 한 가지만 해라.&lt;/h2&gt;
&lt;p&gt;아래의 함수는 몇 가지를 수행하는 것일까?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1) 페이지가 테스트 페이지인지 판단한다.&lt;/p&gt;
&lt;p&gt;2) 맞다면 설정 페이지/해제 페이지를 넣는다.&lt;/p&gt;
&lt;p&gt;3) 페이지를 렌더링한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;함수 이름 아래에서 추상화 수준이 한 단계인 행위를 수행하면 한 가지 동작을 하는 것이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;한 가지의 개념을 추상화 수준에서 생각하고 일관되게 작성하도록 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 서술적인 이름을 사용하라.&lt;/h2&gt;
&lt;p&gt;함수 이름을 보고 짐작한 동작이 그대로 작동하는 이름이 좋다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ex) isTestable, setup&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 함수 매개변수&lt;/h2&gt;
&lt;p&gt;매개 변수를 최대한 적게 작성하라.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3개 이상은 가급적 피하고, 4개 이상은 특별한 이유가 있어야 한다. (특별한 이유가 있어도 사용하지 마라)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;테스트 관점에서 생각해보자&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;매개 변수가 1개인 함수를 테스트하려고 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;매개 변수로 들어갈 수 있는 개수가 100개라면 100번을 다 수행해야 정확한 동작을 보장한다고 할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 매개 변수가 2개 이상이라면? 4개 이상이라면?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;많은 매개변수는 동작을 예측하기 힘들게 만든다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 함수 출력&lt;/h2&gt;
&lt;p&gt;함수 매개변수를 출력으로 사용하지 마라.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;append(list, item)보다 list.append(item)이 직관적이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자바의 this는 이럴 때 쓰라고 만든 것이다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 명령과 조회 분리&lt;/h2&gt;
&lt;pre id=&quot;code_1581600276613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private boolean setUser(User user, String value) { ... }

if (setUser(user, min)) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;함수를 작성한 개발자는 setUser를 동사로 의도하고 작성했지만&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;if문에서 boolean의 의미로 사용하는 순간 형용사처럼 느껴진다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그래서 if의 setUser는 'user의 어떤 필드가 min으로 설정되어 있다면' 이라고 해석될 여지가 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 예외처리는 try / catch를 사용하라&lt;/h2&gt;
&lt;pre id=&quot;code_1581600621034&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (condition1) {
    if (user.status == UserStatus.NORMAL) {
        ...
    } else {
        log.error(&quot;error1&quot;);
    }
} else {
    log.error(&quot;error2&quot;);
    return ErrorCode.ERROR;
}

---

try {
    ...
} catch (UserStatusException e) {
    log.error(e.getMessage());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;try/catch 구문을 사용하면 오류를 처리하는 로직을 따로 뺄 수 있어 깔끔하다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>IT 도서/Clean Code</category>
      <category>Clean Code</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/76</guid>
      <comments>https://private-space.tistory.com/76#entry76comment</comments>
      <pubDate>Thu, 13 Feb 2020 22:34:41 +0900</pubDate>
    </item>
    <item>
      <title>[Clean Code] 의미 있는 이름을 사용하라.</title>
      <link>https://private-space.tistory.com/75</link>
      <description>&lt;blockquote data-ke-size=&quot;size26&quot; data-ke-style=&quot;style2&quot;&gt;Clean Code를 읽고 내용을 정리한 것입니다.&lt;br /&gt;&lt;a href=&quot;http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&amp;amp;mallGb=KOR&amp;amp;barcode=9788966260959&amp;amp;orderClick=LEA&amp;amp;Kc=&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;책 구매하러 가기&lt;/a&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 의도를 분명히 밝혀라&lt;/h2&gt;
&lt;pre id=&quot;code_1581511001813&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;boolean flag; // 무슨 의미?

boolean isDarkMode;
boolean isLightMode;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag라는 이름엔 아무 의미가 담겨있지 않다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;isDarkMode / isLightMode처럼 의도가 드러나는 이름을 사용해야 코드 이해와 변경이 쉽다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 그릇된 정보를 피하라&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1) 개발자에게 List는 자료구조를 의미한다. 실제로 List가 아닌 것들은 List라는 의미를 붙이지 않는 편이 좋다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2) 흡사한 이름을 사용하지 않는다. 일관성이 떨어지는 표기법은 오해를 낳는다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;List/Queue 등 자료구조나 기술을 의미하는 단어는 개발자에게 친숙하다.&lt;/p&gt;
&lt;p&gt;실제로 대중적인 기술명을 변수 이름에 담으면 이해하기 더 쉽다.&lt;/p&gt;
&lt;pre id=&quot;code_1581512601362&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; userCountList;
Queue&amp;lt;Integer&amp;gt; jobQueue;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 의미 있게 구분하라&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;컴파일이 목적인 이름은 사용하지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1581511325231&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int a1, a2, a3;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;a1, a2, a3에는 아무런 정보가 들어있지 않으며 붙은 숫자만 다를 뿐이다. 개발자의 의도를 담기 어렵다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이름이 다르다면 의미도 달라져야 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;불용어를 추가한 이름도 정보를 제공하기 어렵다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;keyboardInfo, keyboardData라는 두 변수가 있는 경우 info나 data의 의미를 구분하기 어렵다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 발음하기 쉬운 이름을 사용하라&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;한국 사람들은 외국 사람들에 비해 숫자를 잘 외운다고 한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;456789라는 수가 있을 때, 한국사람들은 한 숫자에 한 글자씩 붙여 '사오육칠팔구'로 외우지만&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;영어권의 사람은 'four five six seven eight nine'으로 읽는다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;한국어로는 6글자로 읽을 수 있지만 영어는 11글자를 읽어야한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코드도 마찬가지다. 읽기 쉬워야 기억하기도 쉽고 이해하기도 쉽다.&lt;/p&gt;
&lt;pre id=&quot;code_1581511813408&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;long ctm;
long createTime;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 대상의 타입에 따라 적절한 형태의 단어를 사용하라&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1) 클래스나 객체는 명사/명사구가 적절하며 동사는 사용하지 않는다.&lt;/p&gt;
&lt;p&gt;2) 메소드명은 동사/동사구가 적합하다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;생성자가 오버로드가 되어 있다면 정적 팩토리 메소드 패턴을 사용하는 것이 편하다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1581512257395&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;User user = new User(userId);
User user = User.fromUserId(userId); // 훨씬 보기 편하다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 한 개념에 한 단어를 사용하라.&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;DataManager&lt;/p&gt;
&lt;p&gt;DataConfiguration&lt;/p&gt;
&lt;p&gt;DataDriver&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 Manager / Configuration / Driver에서 하는 역할이 동일하다면 같은 단어로 통일해서 사용한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;몇 가지 단순한 원칙만 지켜도 코드 가독성이 높아지고 생산성이 향상됨을 느낄 수 있다.&lt;/p&gt;</description>
      <category>IT 도서/Clean Code</category>
      <category>Clean Code</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/75</guid>
      <comments>https://private-space.tistory.com/75#entry75comment</comments>
      <pubDate>Wed, 12 Feb 2020 22:07:25 +0900</pubDate>
    </item>
    <item>
      <title>TDD 실천법과 도구</title>
      <link>https://private-space.tistory.com/74</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://repo.yona.io/doortts/blog/issue/1&quot;&gt;https://repo.yona.io/doortts/blog/issue/1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1581496191007&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;&amp;quot;TDD 실천법과 도구&amp;quot; 책 전체를 PDF 공개합니다.&quot; data-og-description=&quot;2010년 6월에 출간되었던 &amp;quot;TDD 실천법과 도구&amp;quot; 책 전체를 PDF로 공개합니다. 책소개: http://naver.me/GaYZCDjD Updated --- - [1장 - 테스트주도개발 Test Driven Development](https://repo.yona.io/doortts/blog/issue/2) - 18.07.18 - [2장 - doortts/blog&quot; data-og-host=&quot;repo.yona.io&quot; data-og-source-url=&quot;https://repo.yona.io/doortts/blog/issue/1&quot; data-og-url=&quot;https://repo.yona.io/doortts/blog/issue/1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dWomfC/hyEVATOMrA/9FuyQYfRTVkOBDt1pgeS00/img.png?width=510&amp;amp;height=473&amp;amp;face=0_0_510_473,https://scrap.kakaocdn.net/dn/Pm8Ma/hyEUbOTuFf/ShSbsaPSnMnOJKj3CNn150/img.png?width=449&amp;amp;height=523&amp;amp;face=0_0_449_523&quot;&gt;&lt;a href=&quot;https://repo.yona.io/doortts/blog/issue/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://repo.yona.io/doortts/blog/issue/1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dWomfC/hyEVATOMrA/9FuyQYfRTVkOBDt1pgeS00/img.png?width=510&amp;amp;height=473&amp;amp;face=0_0_510_473,https://scrap.kakaocdn.net/dn/Pm8Ma/hyEUbOTuFf/ShSbsaPSnMnOJKj3CNn150/img.png?width=449&amp;amp;height=523&amp;amp;face=0_0_449_523');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;&quot;TDD 실천법과 도구&quot; 책 전체를 PDF 공개합니다.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;2010년 6월에 출간되었던 &quot;TDD 실천법과 도구&quot; 책 전체를 PDF로 공개합니다. 책소개: http://naver.me/GaYZCDjD Updated --- - [1장 - 테스트주도개발 Test Driven Development](https://repo.yona.io/doortts/blog/issue/2) - 18.07.18 - [2장 - doortts/blog&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;repo.yona.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;작성자 분께서 책이 절판되었다고 PDF로 배포하셨다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;틈틈히 들어가서 봐야겠다.&lt;/p&gt;</description>
      <category>IT 도서</category>
      <category>TDD</category>
      <category>TDD 실천법과 도구</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/74</guid>
      <comments>https://private-space.tistory.com/74#entry74comment</comments>
      <pubDate>Wed, 12 Feb 2020 17:30:56 +0900</pubDate>
    </item>
    <item>
      <title>2019 회고록과 새로운 다짐</title>
      <link>https://private-space.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1월이 다 지나가는 지금, 2019년을 돌아보고자 한다.&lt;/p&gt;
&lt;h1&gt;2019년&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2019년의 목표가 몇 가지 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제나 궁극적인 목표는 행복한 삶이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 2019년엔 잘못 보냈다고 생각했던 2018년을 바로잡고자 했던 마음이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여러가지를 도전했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;토익 스피킹&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토익 스피킹은 그리 어려운 주제까진 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 달 남짓 공부하고 필요한 만큼의 등급을 따냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 여유가 남는다면 오픽도 도전해보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;삼성 알고리즘 테스트 B형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5-6월 경 준비를 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자는 어려울 것이라 판단하여 오프라인 스터디 그룹을 만들고 모의 문제를 만들고 풀이하는 방법으로 공부했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A형은 쉽게 딸 수 있는 것에 비해 B형의 악명은 유명한 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;B형 시험은 문제에서 요구하는 API를 작성해야 하며 문제에 모든 조건이 주어지지 않기 때문에 메인 소스를 보고 파악해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 이 시험은 다른 테스트와는 구별되는 몇 가지 특징이 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째로 라이브러리 사용에 제약이다.&lt;br /&gt;C의 malloc과 같이 필수불가결 및 대체 불가능 수준의 라이브러리 코드를 제외하곤 절대 사용할 수 없다. 물론 STL(자료구조)도 못쓴다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 최적화 여부에 따라 합/불 여부가 갈릴 수 있다. 시간/메모리 제약 등 모든 조건을 만족해도 불합격이 나올 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세간엔 코드 리뷰도 진행하여 너무 이상하게 작성했다면 탈락한다는 이야기도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 특징들로 인해 주요 자료구조를 직접 구현할 수 있어야 하고 다양한 방면에 응용까지 가능한 능력이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험을 위해 준비했었던 것들을 기억나는 대로 나열해보면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;Hash&lt;/li&gt;
&lt;li&gt;Linked List(Hash 충돌 방지-Chaining 기법에 활용)&lt;/li&gt;
&lt;li&gt;Queue&lt;/li&gt;
&lt;li&gt;Stack&lt;/li&gt;
&lt;li&gt;Union find&lt;/li&gt;
&lt;li&gt;Heap&lt;/li&gt;
&lt;li&gt;Graph&lt;/li&gt;
&lt;li&gt;Tree&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도가 있는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 문제가 어떻느냐에 상관없이 가장 유용했던 것은 1, 2, 3, 4, 6이었다.&lt;br /&gt;(특히 해시는 B형 시험에서 유용하다의 수준을 넘어 진리에 가깝다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'문제를 풀 수나 있을까' 하는 걱정을 안고 7월 첫 시험에 응시했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 문제가 내 눈 앞에 펼쳐졌을 때엔 꽤 많이 당황스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 더 어려웠기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30분 정도 곰곰히 풀이법을 생각하다보니 적절한 풀이법이 생각났다. 코드 작성은 금방 완료되었고 테스트 케이스 전부를 다 맞추었다. 최적화를 더 이상 안될 정도까지 해 본 뒤 시험장을 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 단톡방에서 풀이를 공유했는데, 괜찮게 푼 것 같아 기대를 아주 많이 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉽게도 결과는 불합격이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 시험은 9월이었다. 메인 소스에 이미 작성되어 있는 API를 호출할 수 있고 그 횟수를 최소한으로 줄여야 하는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체감상 이 때가 구현 난이도와 상관없이 가장 어려웠던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험장을 나오면서 기대를 아예 하지 않았기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 11월 시험은 엑셀과 비슷한 것을 구현하는 문제가 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보자마자 풀이법이 생각나서 약 한 시간이 넘은 시점에 코드 작성을 완료했고 디버깅에 최적화까지 마치니 세 시간이 흘렀다. 더 이상은 할 수 있는 것이 없어 시험 시간을 30분 정도 남기고 시험장을 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대를 잔뜩 했는데 결과는 또 불합격이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 어느 부분에서 실수했는지 감도 잡히지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 1월에도 도전할까 했지만, 무엇을 위해 이 시험을 준비하고 시간을 들이는지에 대한 허탈감이 들었다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 시험엔 응시하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 시험엔 심기일전하고 다시 도전해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;2020년&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 올해는 무엇을 진행해야 할 지 잘 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 실패가 너무 가슴아프기도 하고 그 실패로 인해 이룬 것이 없다는 생각이 들어 정신적으로 조금 지쳤는지도 모른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 행복한 삶을 위해 더 나은 사람이 되고 싶은 욕망은 아직도 크다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 정말 해야겠다 싶은 것, 작년에 못했던 것들을 올해의 목표로 삼아보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;블로그 활성화하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2017년엔 알고리즘을 공부하다가 같은 문제를 몇 달 뒤에 볼 때 기억이 나지 않아 블로그를 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2018년 취업을 하면서 자연스럽게 알고리즘을 손에서 놓게 되고 블로그 포스팅도 중단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2019년 연말부터 다시 기록을 시작하자는 마음이 갑자기 들어서 티스토리에 마크다운으로 업로드 하는 방법을 찾아보기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무를 하면서 마크다운으로 작성했던 것들을 업로드 하려고 했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2019년 초부터 티스토리 에디터 차원에서 마크다운을 지원함에 따라 다시 포스팅을 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2020년엔 공부했던 모든 내용을 정리하여 올리고 다시 돌아볼 수 있는 공간으로 가꾸어보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;개인 서비스 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연말연시 즈음 AWS, GCP 프리티어 서버를 개설했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학부시절부터 IT 서비스를 해보고 싶은 마음이 컸던 것도 있고 클라우드 서버 경험을 쌓아보고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 토이프로젝트 겸 개인 서비스를 오픈하여 운영하는 것이 목표이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;오픈소스 커뮤니티 활동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세상에 선한 영향을 끼치고 싶은 개발자가 되고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 생각하는 '선한 영향을 끼치는 개발자'는 오픈소스 커뮤니티 활동을 통해 많은 사람들에게 더 나은 지식을 전파하는 사람이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 해야할지 잘 모르겠어서 요새는 페이스북 개발자 관련 그룹을 자주 들여다 보고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 프로젝트나 커뮤니티에 활발하게 참여할 계기를 하나쯤은 마련해보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2020년 연말, 한 해를 돌아보았을 때 다짐했던 것들을 모두 이루어내는 한 해가 되기를 바란다.&lt;/p&gt;</description>
      <category>기타/회고</category>
      <category>2019</category>
      <category>2019회고</category>
      <category>회고</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/72</guid>
      <comments>https://private-space.tistory.com/72#entry72comment</comments>
      <pubDate>Wed, 29 Jan 2020 19:27:39 +0900</pubDate>
    </item>
    <item>
      <title>Spring framework 소스 코드 읽어보기 - Bean 생성 원리 (2)</title>
      <link>https://private-space.tistory.com/71</link>
      <description>&lt;h1&gt;Bean 생성&lt;/h1&gt;
&lt;h2&gt;AnnotationConfigApplicationContext&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AnnotationConfigApplicationContext&lt;/code&gt; 클래스는 어노테이션을 읽어 bean을 등록하는 역할을 담당하고 있다.&lt;/p&gt;
&lt;p&gt;그 중에서도 기본 생성자에 포함되어 반드시 호출하게 되어 있는 &lt;strong&gt;register&lt;/strong&gt;라는 메소드를 살펴보았다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * Register one or more component classes to be processed.
     * &amp;lt;p&amp;gt;Note that {@link #refresh()} must be called in order for the context
     * to fully process the new classes.
     * @param componentClasses one or more component classes &amp;amp;mdash; for example,
     * {@link Configuration @Configuration} classes
     * @see #scan(String...)
     * @see #refresh()
     */
    @Override
    public void register(Class&amp;lt;?&amp;gt;... componentClasses) {
        Assert.notEmpty(componentClasses, &amp;quot;At least one component class must be specified&amp;quot;);
        this.reader.register(componentClasses);
    }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;새 클래스를 완전히 처리하기 위해 반드시 호출된다고 한다.&lt;/p&gt;
&lt;p&gt;여기의 두 번째 doRegisterBean 메소드를 따라가면 &lt;code&gt;AnnotatedBeanDefinitionReader&lt;/code&gt; 클래스에 도달한다.&lt;/p&gt;
&lt;h3&gt;AnnotatedBeanDefinitionReader&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AnnotatedBeanDefinitionReader&lt;/code&gt;의 주석에는 &lt;strong&gt;Convenient adapter for programmatic registration of bean classes&lt;/strong&gt; 라는 설명이 붙어 있다.&lt;/p&gt;
&lt;p&gt;그리고 아까 확인하고자 했던 doRegisterBean의 java doc에는 &lt;strong&gt;Register a bean from the given bean class, deriving its metadata from class-declared annotations.&lt;/strong&gt; 라고 설명되어 있다.&lt;/p&gt;
&lt;p&gt;개발자가 annotation 기반으로 bean을 생성하면 이 클래스의 doRegisterBean 메소드에서 등록을 담당하는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * Register a bean from the given bean class, deriving its metadata from
     * class-declared annotations.
     * @param beanClass the class of the bean
     * @param name an explicit name for the bean
     * @param qualifiers specific qualifier annotations to consider, if any,
     * in addition to qualifiers at the bean class level
     * @param supplier a callback for creating an instance of the bean
     * (may be {@code null})
     * @param customizers one or more callbacks for customizing the factory&amp;#39;s
     * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag
     * @since 5.0
     */
    private &amp;lt;T&amp;gt; void doRegisterBean(Class&amp;lt;T&amp;gt; beanClass, @Nullable String name,
            @Nullable Class&amp;lt;? extends Annotation&amp;gt;[] qualifiers, @Nullable Supplier&amp;lt;T&amp;gt; supplier,
            @Nullable BeanDefinitionCustomizer[] customizers) {

        AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
        if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
            return;
        }

        abd.setInstanceSupplier(supplier);
        ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
        abd.setScope(scopeMetadata.getScopeName());
        String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));

        AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
        if (qualifiers != null) {
            for (Class&amp;lt;? extends Annotation&amp;gt; qualifier : qualifiers) {
                if (Primary.class == qualifier) {
                    abd.setPrimary(true);
                }
                else if (Lazy.class == qualifier) {
                    abd.setLazyInit(true);
                }
                else {
                    abd.addQualifier(new AutowireCandidateQualifier(qualifier));
                }
            }
        }
        if (customizers != null) {
            for (BeanDefinitionCustomizer customizer : customizers) {
                customizer.customize(abd);
            }
        }

        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
        definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
    }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 메소드는 bean의 메타데이터를 저장하는 클래스를 몇 개 사용하고 있다.&lt;/p&gt;
&lt;p&gt;맨 첫줄의 &lt;code&gt;AnnotatedGenericBeanDefinition&lt;/code&gt; 는 bean의 정의를 갖는 클래스이다.&lt;/p&gt;
&lt;p&gt;부모 클래스를 추적하다 보면 추상 클래스인 &lt;code&gt;AbstractBeanDefinition&lt;/code&gt;에 도달하게 된다.&lt;/p&gt;
&lt;h4&gt;AbstractBeanDefinition&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;AbstractBeanDefinition&lt;/code&gt;는 bean의 속성을 정의하는 클래스이다.&lt;/p&gt;
&lt;p&gt;bean을 생성할 때 굉장히 많은 옵션을 사용할 수 있는데,&lt;/p&gt;
&lt;p&gt;(Autowired를 주입받는 기준이라던가, Primary bean으로 생성할 것인지, lazy 로딩 옵션을 사용할 것인지 등)&lt;/p&gt;
&lt;p&gt;이 곳에 그 정보들이 담겨있다.&lt;/p&gt;
&lt;p&gt;위부터 필드를 몇 개 적어보았다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Scope 관련 (static final 변수)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String SCOPE_DEFAULT : bean의 scope (기본은 싱글턴이라고 주석에 설명되어 있다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AUTOWIRE 옵션 (static final 변수)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;int AUTOWIRE_NO&lt;/li&gt;
&lt;li&gt;int AUTOWIRE_BY_NAME&lt;/li&gt;
&lt;li&gt;int AUTOWIRE_BY_TYPE&lt;/li&gt;
&lt;li&gt;int AUTOWIRE_CONSTRUCTOR&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;의존성 점검 옵션 (static final 변수)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;int DEPENDENCY_CHECK_NONE&lt;/li&gt;
&lt;li&gt;int DEPENDENCY_CHECK_OBJECTS&lt;/li&gt;
&lt;li&gt;int DEPENDENCY_CHECK_SIMPLE&lt;/li&gt;
&lt;li&gt;DEPENDENCY_CHECK_SIMPLE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;객체 저장&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Object beanClass : 스프링이 관리하는 bean의 실체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;... 등등 굉장히 많은 필드가 있다. 다 쓰긴 너무 많으니 직접 살펴보자.&lt;/p&gt;
&lt;p&gt;bean을 인스턴스로 직접 생성하지 않고 정보를 저장한다는 점이 중요한 부분이다.&lt;/p&gt;
&lt;p&gt;아마 bean scope(프로토타입인 경우)에 따라 인스턴스를 계속 생성해야 하기 때문인 것으로 생각된다.&lt;/p&gt;
&lt;p&gt;다음으로 볼 수 있는 클래스는 &lt;code&gt;ScopeMetadata&lt;/code&gt;이다.&lt;/p&gt;
&lt;h4&gt;ScopeMetadata&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ScopeMetadata&lt;/code&gt; 는 bean Scope의 메타 데이터를 저장한다.&lt;/p&gt;
&lt;p&gt;클래스를 열자마자 맨 윗줄에 중요한 정보 하나를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private String scopeName = BeanDefinition.SCOPE_SINGLETON;
private ScopedProxyMode scopedProxyMode = ScopedProxyMode.NO;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Default scope는 싱글톤이며 proxy 모드를 사용하지 않는다.&lt;br&gt;(proxy 모드는 bean scope를 공부하면 알 수 있다)&lt;/p&gt;
&lt;h4&gt;BeanDefinitionHolder&lt;/h4&gt;
&lt;p&gt;bean 생성 과정에서 마지막으로 볼 수 있는 메타 데이터 클래스이다.&lt;/p&gt;
&lt;p&gt;Spring 개발자들은 이 클래스를 이렇게 설명하고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Holder for a BeanDefinition with name and aliases.
 * Can be registered as a placeholder for an inner bean.
 *
 * &amp;lt;p&amp;gt;Can also be used for programmatic registration of inner bean
 * definitions. If you don&amp;#39;t care about BeanNameAware and the like,
 * registering RootBeanDefinition or ChildBeanDefinition is good enough.
 *
 * @author Juergen Hoeller
 * @since 1.0.2
 * @see org.springframework.beans.factory.BeanNameAware
 * @see org.springframework.beans.factory.support.RootBeanDefinition
 * @see org.springframework.beans.factory.support.ChildBeanDefinition
 */&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;name과 alias를 갖는 홀더이며 &lt;code&gt;BeanDefinitionReaderUtils&lt;/code&gt; 의 registerBeanDefinition 메소드 인자로 넘겨주기 위해 사용한다.&lt;/p&gt;
&lt;p&gt;메소드로 넘겨주는 두 번째 인자는 &lt;code&gt;BeanDefinitionRegistry&lt;/code&gt; 인터페이스인데, 실질적으로 bean이 등록되는 곳이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * Register the given bean definition with the given bean factory.
     * @param definitionHolder the bean definition including name and aliases
     * @param registry the bean factory to register with
     * @throws BeanDefinitionStoreException if registration failed
     */
    public static void registerBeanDefinition(
            BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
            throws BeanDefinitionStoreException {

        // Register bean definition under primary name.
        String beanName = definitionHolder.getBeanName();
        registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

        // Register aliases for bean name, if any.
        String[] aliases = definitionHolder.getAliases();
        if (aliases != null) {
            for (String alias : aliases) {
                registry.registerAlias(beanName, alias);
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;메소드 위에서부터 두 번째 라인에 있는 메소드를 보자.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;이 메소드를 설명하는 주석엔 &amp;quot;&lt;strong&gt;Register a new bean definition with this registry.&lt;/strong&gt;&amp;quot; 라고 적혀있다.&lt;/p&gt;
&lt;p&gt;registerBeanDefinition의 정의를 타고 올라가면 &lt;code&gt;DefaultListableBeanFactory&lt;/code&gt; 클래스에 도달한다.&lt;/p&gt;
&lt;p&gt;아래는 해당 클래스의 registerBeanDefinition 메소드 원문이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//---------------------------------------------------------------------
// Implementation of BeanDefinitionRegistry interface
//---------------------------------------------------------------------

@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
        throws BeanDefinitionStoreException {

    Assert.hasText(beanName, &amp;quot;Bean name must not be empty&amp;quot;);
    Assert.notNull(beanDefinition, &amp;quot;BeanDefinition must not be null&amp;quot;);

    if (beanDefinition instanceof AbstractBeanDefinition) {
        try {
            ((AbstractBeanDefinition) beanDefinition).validate();
        }
        catch (BeanDefinitionValidationException ex) {
            throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                    &amp;quot;Validation of bean definition failed&amp;quot;, ex);
        }
    }

    BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
    if (existingDefinition != null) {
        if (!isAllowBeanDefinitionOverriding()) {
            throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
        }
        else if (existingDefinition.getRole() &amp;lt; beanDefinition.getRole()) {
            // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
            if (logger.isInfoEnabled()) {
                logger.info(&amp;quot;Overriding user-defined bean definition for bean &amp;#39;&amp;quot; + beanName +
                        &amp;quot;&amp;#39; with a framework-generated bean definition: replacing [&amp;quot; +
                        existingDefinition + &amp;quot;] with [&amp;quot; + beanDefinition + &amp;quot;]&amp;quot;);
            }
        }
        else if (!beanDefinition.equals(existingDefinition)) {
            if (logger.isDebugEnabled()) {
                logger.debug(&amp;quot;Overriding bean definition for bean &amp;#39;&amp;quot; + beanName +
                        &amp;quot;&amp;#39; with a different definition: replacing [&amp;quot; + existingDefinition +
                        &amp;quot;] with [&amp;quot; + beanDefinition + &amp;quot;]&amp;quot;);
            }
        }
        else {
            if (logger.isTraceEnabled()) {
                logger.trace(&amp;quot;Overriding bean definition for bean &amp;#39;&amp;quot; + beanName +
                        &amp;quot;&amp;#39; with an equivalent definition: replacing [&amp;quot; + existingDefinition +
                        &amp;quot;] with [&amp;quot; + beanDefinition + &amp;quot;]&amp;quot;);
            }
        }
        this.beanDefinitionMap.put(beanName, beanDefinition);
    }
    else {
        if (hasBeanCreationStarted()) {
            // Cannot modify startup-time collection elements anymore (for stable iteration)
            synchronized (this.beanDefinitionMap) {
                this.beanDefinitionMap.put(beanName, beanDefinition);
                List&amp;lt;String&amp;gt; updatedDefinitions = new ArrayList&amp;lt;&amp;gt;(this.beanDefinitionNames.size() + 1);
                updatedDefinitions.addAll(this.beanDefinitionNames);
                updatedDefinitions.add(beanName);
                this.beanDefinitionNames = updatedDefinitions;
                removeManualSingletonName(beanName);
            }
        }
        else {
            // Still in startup registration phase
            this.beanDefinitionMap.put(beanName, beanDefinition);
            this.beanDefinitionNames.add(beanName);
            removeManualSingletonName(beanName);
        }
        this.frozenBeanDefinitionNames = null;
    }

    if (existingDefinition != null || containsSingleton(beanName)) {
        resetBeanDefinition(beanName);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;첫 주석부터 &lt;code&gt;BeanDefinitionRegistry&lt;/code&gt;의 구현체라고 쓰여 있는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;중간에 나타나는 &lt;code&gt;BeanDefinition&lt;/code&gt;은 beanDefinitionMap 필드에서 get(beanDefinitionMap) 메소드를 실행 결과를 저장한다.&lt;/p&gt;
&lt;p&gt;beanDefinitionMap은 뭘까?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** Map of bean definition objects, keyed by bean name. */
private final Map&amp;lt;String, BeanDefinition&amp;gt; beanDefinitionMap = new ConcurrentHashMap&amp;lt;&amp;gt;(256);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;beanDefinitionMap은 이름 그대로 beanDefinition을 저장한 Map이었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BeanDefinition&lt;/code&gt;을 저장하는 과정을 마저 살펴보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
    ...
} else {
    ...
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;BeanDefinition&lt;/code&gt;을 beanDefinitionMap에서 꺼내온다.&lt;/p&gt;
&lt;p&gt;현재 등록 중인 bean은 이전에 등록한 적이 없는 새삥이다.&lt;/p&gt;
&lt;p&gt;개발자는 if문이 아니라 else문에서 bean의 정의를 저장한다고 의도했을 것이다.&lt;/p&gt;
&lt;p&gt;else문을 확인해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;else {
    if (hasBeanCreationStarted()) {
        // Cannot modify startup-time collection elements anymore (for stable iteration)
        synchronized (this.beanDefinitionMap) {
            this.beanDefinitionMap.put(beanName, beanDefinition);
            List&amp;lt;String&amp;gt; updatedDefinitions = new ArrayList&amp;lt;&amp;gt;(this.beanDefinitionNames.size() + 1);
            updatedDefinitions.addAll(this.beanDefinitionNames);
            updatedDefinitions.add(beanName);
            this.beanDefinitionNames = updatedDefinitions;
            removeManualSingletonName(beanName);
        }
    }
    else {
        // Still in startup registration phase
        this.beanDefinitionMap.put(beanName, beanDefinition);
        this.beanDefinitionNames.add(beanName);
        removeManualSingletonName(beanName);
    }
    this.frozenBeanDefinitionNames = null;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;첫 문장부터 조건문이 나온다.&lt;/p&gt;
&lt;p&gt;이름만 보면 Spring의 라이프사이클 상 bean 생성 단계가 시작되었냐는 것으로 생각된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * Check whether this factory&amp;#39;s bean creation phase already started,
  * i.e. whether any bean has been marked as created in the meantime.
  * @since 4.2.2
  * @see #markBeanAsCreated
  */
protected boolean hasBeanCreationStarted() {
    return !this.alreadyCreated.isEmpty();
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;실제로 주석을 살펴보면 그렇다고 한다.&lt;/p&gt;
&lt;p&gt;그럼 여기서도 else문으로 이동한다.&lt;/p&gt;
&lt;p&gt;아직은 bean 등록 단계이기 때문이다.&lt;/p&gt;
&lt;p&gt;else문에서는 beanDefinitionMap에 실제로 bean 정의 데이터를 저장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.beanDefinitionMap.put(beanName, beanDefinition);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;BeanDefinition&lt;/code&gt;은 이름값을 아주 잘 하는 인터페이스로, scope 등 bean에 대한 정의가 담겨 있다.&lt;/p&gt;
&lt;p&gt;더 자세한 정보는 &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/BeanDefinition.html&quot;&gt;공식 레퍼런스&lt;/a&gt;를 참조하자.&lt;/p&gt;
&lt;p&gt;이 곳에 bean의 정의를 등록하는 것으로 생성을 위한 절차는 끝난다.&lt;/p&gt;
&lt;p&gt;다음엔 정의가 등록된 bean의 인스턴스를 어떻게 생성하는지 탐구해볼 예정이다.&lt;/p&gt;</description>
      <category>Java/Spring framework</category>
      <category>bean</category>
      <category>Spring</category>
      <author>감동이중요해</author>
      <guid isPermaLink="true">https://private-space.tistory.com/71</guid>
      <comments>https://private-space.tistory.com/71#entry71comment</comments>
      <pubDate>Mon, 27 Jan 2020 16:24:40 +0900</pubDate>
    </item>
  </channel>
</rss>