<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>gmelon.dev</title>
    <link>https://gmelon.dev/</link>
    <description>백엔드 개발을 공부하고 있습니다.</description>
    <language>ko</language>
    <pubDate>Sat, 2 May 2026 02:40:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>gmelon</managingEditor>
    <image>
      <title>gmelon.dev</title>
      <url>https://tistory1.daumcdn.net/tistory/5318599/attach/7838cc0466114a5983573caabf4fc8ab</url>
      <link>https://gmelon.dev</link>
    </image>
    <item>
      <title>왜 서로 다른 값을 INSERT 했는데 데드락이 걸릴까?</title>
      <link>https://gmelon.dev/182</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 서로 다른 트랜잭션에서 겹치지 않는 FK 값을 가진 엔티티들을 동시에 삽입할 때 데드락이 발생한 케이스가 있었다. 분명 범위가 겹치지 않는데 왜 데드락이 걸리는 걸까?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 결론: 유니크/외래 키 제약조건이 데드락을 유발한다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외래 키(Foreign Key)&lt;/b&gt;이거나 &lt;b&gt;유니크 제약 조건(Unique Constraint)&lt;/b&gt;이 걸려 있다면, 데드락 발생 확률이 비약적으로 높아진다.&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;b&gt;상황&lt;/b&gt;: FK 에는 유니크 제약 조건이 걸려있고 기존 데이터는 10, 20 이 존재한다고 가정. 격리 수준은 REPEATABLE READ.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 A (INSERT 12)&lt;/b&gt;: 유니크 체크를 위해 10~20 사이에 &lt;b&gt;공유 잠금(S-Lock, 갭 락)&lt;/b&gt;을 획득&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 B (INSERT 14)&lt;/b&gt;: 마찬가지로 10~20 사이에 &lt;b&gt;공유 잠금(S-Lock)&lt;/b&gt;을 요청. S-Lock끼리는 호환되므로 &lt;b&gt;B도 락을 획득&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 A (실제 쓰기 시도)&lt;/b&gt;: S-Lock을 X-Lock으로 업그레이드 필요. 하지만 B가 S-Lock을 잡고 있어 &lt;b&gt;대기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 B (실제 쓰기 시도)&lt;/b&gt;: 마찬가지로 A가 S-Lock을 잡고 있어 &lt;b&gt;대기&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과: A는 B를 기다리고, B는 A를 기다리는 전형적인 데드락(Deadlock)에 빠지게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 &quot;겹치지 않는 값&quot;인데도 문제가 되는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 &lt;b&gt;갭 락의 범위&lt;/b&gt;다. 트랜잭션 A가 12를, 트랜잭션 B가 14를 삽입하려 해도, 둘 다 &lt;b&gt;10~20이라는 동일한 갭 구간&lt;/b&gt;에 대해 공유 잠금을 걸게 된다. 값 자체는 다르지만, 잠금 범위가 겹치기 때문에 데드락이 발생하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;격리 수준에 따른 차이&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;격리 수준&lt;/th&gt;
&lt;th&gt;갭 락(Gap Lock) 사용&lt;/th&gt;
&lt;th&gt;10~20 사이 INSERT 시 동작 (유니크/FK 존재 시)&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;REPEATABLE READ&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사용함 (기본)&lt;/td&gt;
&lt;td&gt;A와 B가 10~20 구간 전체를 공유 잠금(S-Lock)으로 잡음&lt;/td&gt;
&lt;td&gt;&lt;b&gt;데드락 (Deadlock)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;READ COMMITTED&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사용 안 함&lt;/td&gt;
&lt;td&gt;A는 12, B는 14 위치만 각각 점유&lt;/td&gt;
&lt;td&gt;&lt;b&gt;성공 (Success)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 배경 지식: 왜 이런 일이 일어나는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 데드락 메커니즘을 제대로 이해하려면 InnoDB의 잠금 방식에 대한 배경 지식이 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;갭 락(Gap Lock)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;갭 락(Gap Lock)&lt;/b&gt;은 MySQL InnoDB 스토리지 엔진에서 사용되는 독특한 잠금 방식으로, &lt;b&gt;레코드 자체가 아니라 레코드와 레코드 사이의 '간격(Gap)'을 잠그는 기능&lt;/b&gt;이다. 일반적인 '레코드 락'이 실제 존재하는 데이터 줄(Row)을 잠그는 것이라면, 갭 락은 &lt;b&gt;데이터가 존재하지 않는 '비어 있는 공간'&lt;/b&gt;을 잠그는 것이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;잠금 대상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;레코드와 바로 인접한 레코드 사이의 간격&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;핵심 목적&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;새로운 레코드가 생성(INSERT)되는 것을 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 갭 락이 필요한가? (팬텀 리드 방지)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갭 락의 주된 목적은 &lt;b&gt;'팬텀 리드(Phantom Read)'&lt;/b&gt; 현상을 막기 위함이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;팬텀 리드: 트랜잭션 내에서 동일한 쿼리를 두 번 실행했을 때, 첫 번째 쿼리에는 없던 레코드가 다른 트랜잭션의 INSERT 작업으로 인해 두 번째 쿼리에서 갑자기 나타나는(유령처럼 보이는) 현상이다. 갭 락은 이 '사이 공간'에 새로운 데이터가 끼어드는 것을 막아 데이터의 일관성을 보장한다.&lt;/blockquote&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;갭&amp;nbsp;락은&amp;nbsp;트랜잭션&amp;nbsp;도중에&amp;nbsp;예상치&amp;nbsp;못한&amp;nbsp;새로운&amp;nbsp;데이터가&amp;nbsp;추가되는&amp;nbsp;것을&amp;nbsp;방지하는&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;b&gt;격리 수준(Isolation Level)&lt;/b&gt;은 데이터베이스가 잠금을 처리하는 방식, 특히 갭 락(Gap Lock)의 사용 여부를 결정짓는 핵심 요소다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;REPEATABLE READ (InnoDB 기본값)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL InnoDB의 기본 격리 수준인 REPEATABLE READ에서는 데이터 정합성을 위해 &lt;b&gt;넥스트 키 락(Next-Key Lock, 레코드 락 + 갭 락)&lt;/b&gt;을 적극적으로 사용한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;REPEATABLE READ 동작 방식&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;유니크/FK 체크&lt;/b&gt;: 트랜잭션 A가 12를 넣기 위해, 트랜잭션 B가 14를 넣기 위해 중복 여부나 부모 키 존재 여부를 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;갭 락 형성&lt;/b&gt;: 이때 InnoDB는 단순히 12, 14라는 값만 확인하는 것이 아니라, 해당 값이 포함된 범위인 10과 20 사이의 간격(Gap) 전체에 대해 공유 잠금(S-Lock)을 건다. 이를 통해 다른 트랜잭션이 이 범위에 데이터를 넣어 '팬텀 리드(Phantom Read)'가 발생하는 것을 방지한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데드락 발생&lt;/b&gt;: A와 B 모두 10~20 사이의 갭 락(S-Lock)을 획득한 상태에서, 각자 자신의 데이터를 INSERT 하려고 시도한다. INSERT를 하려면 배타적 잠금(X-Lock)이 필요한데, 상대방이 이미 S-Lock을 걸고 놓아주지 않으므로 서로 대기하다가 데드락에 빠진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 데드락 발생 가능성이 매우 높다. 유니크/FK 체크가 &quot;이 범위는 내가 지킨다&quot;라는 갭 락을 유발하기 때문이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;READ COMMITTED - 데드락 회피 가능&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;READ COMMITTED 격리 수준에서는 데이터베이스의 동작 방식이 크게 달라진다. 가장 큰 차이점은 &lt;b&gt;&quot;갭 락(Gap Lock)을 기본적으로 비활성화한다&quot;&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;READ COMMITTED 동작 방식&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;갭 락 미사용&lt;/b&gt;: 이 격리 수준에서는 넥스트 키 락을 사용하지 않고, 오직 레코드 락(Record Lock) 위주로 동작한다. 갭 락은 외래 키 제약 조건 확인이나 중복 키 확인과 같은 아주 특수한 경우를 제외하고는 사용되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개별 잠금&lt;/b&gt;: 트랜잭션 A는 12를 삽입하기 위해 12라는 값에 대해서만 유니크 체크를 수행하고 잠금을 시도한다(인덱스 레코드 간의 '사이'를 잠그지 않음). 트랜잭션 B는 14에 대해서만 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;충돌 없음&lt;/b&gt;: A와 B가 작업하려는 공간이 물리적으로 다르므로(12 vs 14), 10과 20 사이라는 '범위' 때문에 서로 부딪힐 일이 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 데드락이 발생하지 않고 A, B 모두 성공한다. 범위 잠금(갭 락)이 사라졌기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;READ COMMITTED에서는 유니크 제약조건을 어떻게 보장하나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;READ COMMITTED 에서 갭락을 집지 않으면, 유니크 제약조건은 어떻게 보장하는걸까?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Real MySQL 8.0에 따르면, &quot;READ COMMITTED 격리 수준에서는 갭 락을 사용하지 않지만, 외래 키 제약 조건의 확인이나 중복 키 확인(Unique Key Check)을 위해서는 갭 락을 사용한다&quot;고 명시되어 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, REPEATABLE READ(이하 RR)와는 락을 거는 '목적'과 '범위'가 다르다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;RR의 갭 락:&lt;/b&gt; 팬텀 리드(Phantom Read) 방지가 주목적. 따라서 &quot;이 범위(10~20) 사이에 아무것도 넣지 마&quot;라고 &lt;b&gt;공간 전체를 강력하게 잠금.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RC의 갭 락:&lt;/b&gt; &lt;b&gt;제약 조건 확인(Constraint Check)&lt;/b&gt;이 주목적. &quot;내가 넣으려는 이 값(12)이 중복되는지, 혹은 부모 키가 있는지만 확인하겠다&quot;는 의도로 동작.&lt;/li&gt;
&lt;/ol&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&gt;&lt;b&gt;REPEATABLE READ (데드락 발생)&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;A가 12를 넣기 위해 &lt;code&gt;10~20&lt;/code&gt; 사이의 &lt;b&gt;모든 갭&lt;/b&gt;을 잠금&lt;/li&gt;
&lt;li&gt;B가 14를 넣기 위해 &lt;code&gt;10~20&lt;/code&gt; 사이의 &lt;b&gt;모든 갭&lt;/b&gt;을 잠금&lt;/li&gt;
&lt;li&gt;서로가 같은 Gap을 잠그고 비켜주지 않으므로 충돌&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;READ COMMITTED (성공)&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;RC에서는 갭 락을 사용하더라도, 그 범위가 &lt;b&gt;삽입하려는 특정 인덱스 레코드나 그 인접한 위치의 검증&lt;/b&gt;으로 제한됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱스 유니크 스캔:&lt;/b&gt; 데이터를 넣기 전, 해당 값이 인덱스에 존재하는지 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최소한의 잠금:&lt;/b&gt; 값이 없다면 &lt;b&gt;'해당 키 값(예: 12)'&lt;/b&gt;에 대해서만 다른 트랜잭션이 끼어들지 못하게 제어. RR처럼 광범위한 범위에 락을 걸지 않음&lt;/li&gt;
&lt;li&gt;결과적으로 A는 12, B는 14라는 &lt;b&gt;서로 다른 물리적 대상&lt;/b&gt;을 다루게 되어 충돌 없이 동시에 삽입에 성공함.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 READ COMMITTED에서도 유니크/FK 확인을 위해 내부적으로 특수한 형태의 락(갭 락의 일종)이 사용되는 것은 맞지만, 그 범위가 입력하려는 특정 값에 집중되므로, RR에서 발생하는 '불필요한 범위 잠금으로 인한 데드락'을 피할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;넥스트 키 락(Next-Key Lock)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;넥스트 키 락(Next-Key Lock)&lt;/b&gt;은 레코드 락과 갭 락을 합쳐 놓은 형태의 잠금이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;넥스트 키 락 = 레코드 락(Record Lock) + 갭 락(Gap Lock)&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;InnoDB에서는 갭 락을 단독으로 사용하는 경우도 있지만, 대부분 &lt;b&gt;넥스트 키 락&lt;/b&gt; 형태로 사용한다. InnoDB는 레코드를 검색하거나 스캔할 때, 해당 레코드만 잠그는 것이 아니라 &lt;b&gt;그 레코드 앞의 간격(Gap)까지 함께 잠가서&lt;/b&gt; 다른 트랜잭션이 그 사이에 데이터를 넣지 못하도록 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;갭 락 (Gap Lock)&lt;/th&gt;
&lt;th&gt;넥스트 키 락 (Next-Key Lock)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;잠금 대상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;레코드와 레코드 사이의 비어 있는 공간(Gap)&lt;/td&gt;
&lt;td&gt;레코드 자체 + 그 레코드의 바로 앞 간격(Gap)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;레코드 포함 여부&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;❌ (실제 데이터는 잠그지 않음)&lt;/td&gt;
&lt;td&gt;✅ (실제 데이터도 함께 잠금)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주요 목적&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;새로운 데이터의 생성(INSERT) 방지&lt;/td&gt;
&lt;td&gt;레코드 변경 방지 + 새로운 데이터 생성 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사용 시점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;주로 넥스트 키 락의 일부로 사용됨&lt;/td&gt;
&lt;td&gt;InnoDB의 일반적인 범위 검색 시 기본 잠금 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;10번&lt;/b&gt;과 &lt;b&gt;30번&lt;/b&gt; 사원 데이터만 있다고 가정해 보자. (20번은 없음)&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;락 종류&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;레코드 락 (10번)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;10번 사원 데이터만 수정/삭제 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;갭 락 (10~30 사이)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;10과 30 사이의 공간만 잠금. 15번이나 20번 신규 사원 등록(INSERT) 불가능. 하지만 이미 존재하는 10, 30번 데이터 수정은 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;넥스트 키 락 (30번 기준)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;30번 사원 데이터 수정 불가능(레코드 락) + 10과 30 사이 공간에 신규 등록 불가능(갭 락)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10과 30 사이에 &lt;b&gt;갭 락만 단독으로&lt;/b&gt; 걸렸다면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ &lt;b&gt;가능&lt;/b&gt;: 10번이나 30번 레코드의 내용을 수정(UPDATE)하거나 삭제(DELETE)하는 것&lt;/li&gt;
&lt;li&gt;❌ &lt;b&gt;불가능&lt;/b&gt;: 10과 30 사이(예: 15, 20 등)에 새로운 데이터를 입력(INSERT)하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적으로 갭 락은 레코드를 잠그지 않지만, 실제 MySQL InnoDB 환경에서는 &lt;b&gt;넥스트 키 락&lt;/b&gt;이 기본으로 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB에서 범위 검색(예: &lt;code&gt;SELECT ... WHERE id BETWEEN 10 AND 30 FOR UPDATE&lt;/code&gt;)을 수행하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색된 레코드(10, 30)에는 &lt;b&gt;레코드 락&lt;/b&gt;이 걸리고&lt;/li&gt;
&lt;li&gt;그 사이에는 &lt;b&gt;갭 락&lt;/b&gt;이 함께 걸린다&lt;/li&gt;
&lt;li&gt;결과적으로 10, 30 데이터도 수정하지 못하고, 그 사이에 데이터도 넣지 못하는 상태가 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인서트 인텐션 락(Insert Intention Lock)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT 작업 시 갭 락과 어떻게 상호작용하는지 이해하면, 데드락 발생 원리가 더 명확해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;갭 락(Gap Lock)&lt;/b&gt;과 &lt;b&gt;인서트 인텐션 락(Insert Intention Lock)&lt;/b&gt;은 서로 &lt;b&gt;&quot;충돌(Conflict)하여 대기를 유발하는 관계&quot;&lt;/b&gt;다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;락 종류&lt;/th&gt;
&lt;th&gt;역할 (비유)&lt;/th&gt;
&lt;th&gt;관계 설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;갭 락 (Gap Lock)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;통행 금지&quot; 표지판&lt;/td&gt;
&lt;td&gt;특정 범위에 새로운 데이터가 들어오지 못하게 막음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인서트 인텐션 락&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;진입 신호&quot;&lt;/td&gt;
&lt;td&gt;INSERT를 하기 위해 해당 갭에 진입하겠다는 의사 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;대기 (Waiting)&lt;/td&gt;
&lt;td&gt;갭 락이 해제될 때까지 인서트 인텐션 락은 대기 상태에 빠짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;호환성 비교&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;락 조합&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;갭 락 vs 갭 락&lt;/td&gt;
&lt;td&gt;&lt;b&gt;호환됨&lt;/b&gt;. 여러 트랜잭션이 동시에 같은 구간에 갭 락을 걸 수 있음(공유 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;갭 락 vs 인서트 인텐션 락&lt;/td&gt;
&lt;td&gt;&lt;b&gt;충돌함&lt;/b&gt;. 누군가 갭 락을 걸고 있다면, 그 갭이 풀리기 전까지는 새로운 데이터를 넣을 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인서트 인텐션 락 vs 인서트 인텐션 락&lt;/td&gt;
&lt;td&gt;&lt;b&gt;호환됨&lt;/b&gt;. 같은 갭이라도 삽입 위치가 다르면 서로 기다리지 않고 동시에 락을 획득 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;이 호환성 표가 데드락의 핵심이다.&lt;/b&gt; 갭 락끼리는 호환되기 때문에 A와 B가 동시에 같은 구간에 S-Lock(갭 락)을 걸 수 있고, 이후 둘 다 X-Lock으로 업그레이드하려 할 때 서로를 기다리게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;INSERT 시 락이 걸리는 과정 정리&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;인서트 인텐션 락 획득 시도&lt;/b&gt;: 삽입하려는 레코드 사이의 간격에 대해 락을 요청한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 갭 락 확인&lt;/b&gt;: 누군가 그 간격을 막고 있는지(Gap Lock) 확인한다. 막혀 있으면 &lt;b&gt;대기 (Wait)&lt;/b&gt;, 비어 있으면 &lt;b&gt;통과 (인서트 인텐션 락 획득)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레코드 락 획득&lt;/b&gt;: 실제 데이터가 들어갈 공간에 &lt;b&gt;레코드 락(Record Lock)&lt;/b&gt;을 건다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 입력&lt;/b&gt;: 데이터를 쓴다&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT를 하기 위해 일반적인 갭 락을 먼저 잡아야 하는 것은 아니다. 오히려 INSERT는 최소한의 잠금(레코드 락)만으로 처리되도록 설계되어 있으며, 인서트 인텐션 락은 다른 트랜잭션끼리 서로 방해하지 않고 동시에 데이터를 넣을 수 있게 해주는 장치다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 케이스별 잠금 범위&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기부터는 그냥 같이 궁금해서 찾아본 내용&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB 스토리지 엔진이 데이터를 잠그는 범위는 &lt;b&gt;'인덱스(Index)'의 상태&lt;/b&gt;와 &lt;b&gt;'검색 조건'&lt;/b&gt;에 따라 크게 달라진다. InnoDB는 레코드 자체가 아니라 &lt;b&gt;인덱스를 잠그는 방식&lt;/b&gt;으로 작동하기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case A: 유니크 인덱스(PK 등)로 '단건' 조회 시 (Record Lock)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가장 효율적인 케이스&lt;/b&gt;다. 프라이머리 키나 유니크 인덱스를 통해 정확히 1개의 레코드만 조회하는 경우다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;잠금 범위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;해당 &lt;b&gt;레코드(인덱스)&lt;/b&gt;만 딱 잠금&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;갭 락이 필요 없으므로 &lt;b&gt;레코드 락(Record Lock)&lt;/b&gt;만 걸림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case B: 유니크 인덱스가 아니거나, 범위 검색 시 (Next-Key Lock)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 인덱스(Non-Unique Index)를 사용하거나 범위 조건(&lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt; 등)으로 검색하는 경우다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;잠금 범위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;검색된 레코드 + 그 레코드의 &lt;b&gt;바로 앞 간격(Gap)&lt;/b&gt;을 모두 잠금&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;넥스트 키 락&lt;/b&gt;이라고 함. 다른 트랜잭션이 검색 범위 사이에 새로운 데이터를 끼워 넣는 것(INSERT)을 막음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case C: 인덱스가 없는 컬럼으로 업데이트할 때 (Full Table Scan)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가장 위험한 케이스&lt;/b&gt;다. UPDATE나 DELETE 조건에 인덱스가 없는 컬럼을 사용하면 심각한 성능 저하가 발생할 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;잠금 범위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;테이블의 &lt;b&gt;모든 레코드&lt;/b&gt;를 잠금&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;InnoDB는 인덱스를 통해 대상을 찾는데, 사용할 인덱스가 없으면 테이블을 풀 스캔하면서 모든 레코드에 잠금을 건다. 예를 들어, 테이블에 30만 건의 데이터가 있다면 &lt;b&gt;30만 건 전체가 잠김&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;케이스&lt;/th&gt;
&lt;th&gt;잠금 범위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단건 조회(Unique Index)&lt;/td&gt;
&lt;td&gt;해당 레코드 1개만 잠금 (Gap Lock 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;범위/일반 조회&lt;/td&gt;
&lt;td&gt;레코드 + 앞쪽 간격(Gap)까지 잠금 (Next-Key Lock)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱스 미사용&lt;/td&gt;
&lt;td&gt;테이블의 모든 레코드를 잠금&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MySQL은 인덱스를 기반으로 락을 건다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'MySQL은 인덱스를 기반으로 락을 건다'&lt;/b&gt;는 말의 핵심은 &lt;b&gt;&quot;데이터베이스가 실제 변경하려는 레코드뿐만 아니라, 그 레코드를 찾기 위해 검색한 인덱스의 범위까지 모두 잠근다&quot;&lt;/b&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;InnoDB 스토리지 엔진은 레코드 자체(데이터 파일의 행)를 직접 잠그는 것이 아니라, &lt;b&gt;인덱스의 엔트리(Entry)&lt;/b&gt;를 잠그는 방식으로 구현되어 있다. 이로 인해 인덱스 설계에 따라 잠금 범위가 예상보다 훨씬 커질 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: Real MySQL 8.0&lt;/p&gt;
&lt;/blockquote&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;employees&lt;/code&gt;)이 있고, &lt;code&gt;first_name&lt;/code&gt; 컬럼에만 인덱스가 걸려 있다고 가정해 보자.&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;테이블: &lt;code&gt;employees&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;인덱스: &lt;code&gt;ix_first_name&lt;/code&gt; (&lt;code&gt;first_name&lt;/code&gt; 컬럼만 인덱스 생성됨)&lt;/li&gt;
&lt;li&gt;데이터: &lt;code&gt;first_name&lt;/code&gt;이 'Georgi'인 사원은 총 &lt;b&gt;253명&lt;/b&gt;, 그중 &lt;code&gt;last_name&lt;/code&gt;이 'Klassen'인 사원은 &lt;b&gt;1명&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 쿼리&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;UPDATE employees SET hire_date=NOW() 
WHERE first_name='Georgi' AND last_name='Klassen';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 데이터가 잠길까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상식적으로는 조건에 맞는 'Georgi Klassen' &lt;b&gt;1명만&lt;/b&gt; 잠겨야 할 것 같지만, 실제로는 &lt;code&gt;first_name&lt;/code&gt; 인덱스를 통해 검색된 &lt;b&gt;'Georgi' 이름을 가진 253명 전원에게 락이 걸린다&lt;/b&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UPDATE를 수행하기 위해 InnoDB는 &lt;code&gt;ix_first_name&lt;/code&gt; 인덱스를 사용하여 &lt;code&gt;first_name='Georgi'&lt;/code&gt;인 레코드를 먼저 찾는다&lt;/li&gt;
&lt;li&gt;이때 &lt;b&gt;검색된 모든 인덱스 레코드에 잠금&lt;/b&gt;을 걸기 때문이다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;last_name&lt;/code&gt;에 대한 조건은 인덱스에 없으므로, 일단 잠금을 건 뒤에 데이터를 읽어서 확인하는 과정(필터링)을 거친다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 이 쿼리가 실행되는 동안, 다른 트랜잭션은 Georgi라는 이름을 가진 다른 사원(예: 'Georgi Kim')의 정보를 수정할 수 없게 된다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UPDATE 문장이 실행될 때, 조건을 만족하는 레코드를 찾기 위해 &lt;b&gt;테이블의 처음부터 끝까지 모든 레코드를 스캔(Full Table Scan)&lt;/b&gt;해야 한다&lt;/li&gt;
&lt;li&gt;이 과정에서 테이블에 있는 &lt;b&gt;모든 레코드(예: 30만 건)를 다 잠그게&lt;/b&gt; 된다&lt;/li&gt;
&lt;li&gt;즉, 단 한 명의 정보를 바꾸기 위해 전체 테이블이 잠기는 결과를 초래하여, 동시성 처리가 사실상 불가능해진다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 중요한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;인덱스를 기반으로 락을 건다&quot;&lt;/b&gt;는 것은 다음을 의미한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;잠금 범위는 검색 범위와 같다&lt;/b&gt;: 실제 수정되는 데이터가 1건이라도, 인덱스 검색 조건에 걸린 데이터가 1000건이면 1000건 모두 잠긴다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱스 설계가 동시성 성능을 결정한다&lt;/b&gt;: 적절한 인덱스가 없으면 불필요하게 많은 데이터가 잠겨서 다른 작업들이 대기해야 하는 상황(성능 저하)이 발생한다&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 팬텀 리드(Phantom Read)와 격리 수준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갭 락이 존재하는 근본적인 이유를 이해하기 위한 배경 지식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;격리 수준별 팬텀 리드 발생 여부&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;격리 수준 (Isolation Level)&lt;/th&gt;
&lt;th&gt;팬텀 리드 발생 여부&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;READ UNCOMMITTED&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;발생&lt;/td&gt;
&lt;td&gt;커밋되지 않은 데이터도 읽으므로, 다른 트랜잭션이 막 INSERT 한 데이터를 바로 볼 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;READ COMMITTED&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;발생&lt;/td&gt;
&lt;td&gt;다른 트랜잭션이 데이터를 INSERT 하고 COMMIT 하면, 내 트랜잭션이 끝나지 않았어도 두 번째 조회 시 그 데이터를 볼 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;REPEATABLE READ&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;InnoDB는 &lt;b&gt;미발생&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;일반적인 DBMS 표준에서는 발생할 수 있지만, MySQL InnoDB는 &lt;b&gt;넥스트 키 락&lt;/b&gt;을 사용하여 발생하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SERIALIZABLE&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;미발생&lt;/td&gt;
&lt;td&gt;가장 엄격한 수준으로, 읽기 작업도 잠금을 걸어 다른 트랜잭션의 접근을 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;넥스트 키 락의 방지 메커니즘 (REPEATABLE READ)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB는 REPEATABLE READ 격리 수준에서 &lt;b&gt;넥스트 키 락(Next-Key Lock)&lt;/b&gt;을 사용하여 팬텀 리드를 방지한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 원리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 존재하는 레코드만 잠그는 것이 아니라, 레코드가 들어갈 수 있는 &lt;b&gt;'사이 공간(Gap)'까지 잠가버려서&lt;/b&gt; 다른 트랜잭션이 새로운 데이터를 INSERT 하지 못하게 막는다.&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;code&gt;employees&lt;/code&gt; 테이블에 &lt;code&gt;emp_no&lt;/code&gt;(사원번호)가 &lt;b&gt;500,000번&lt;/b&gt;인 사원만 있고, 그 이후 번호는 없는 상태라고 가정해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 A (사용자 A)&lt;/b&gt;가 사원 번호 500,000번 이상의 사원을 조회한다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 트랜잭션 A 시작
BEGIN;
SELECT * FROM employees WHERE emp_no &amp;gt;= 500000 FOR UPDATE;
-- 결과: 사원번호 500,000번 사원 1명 조회됨&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;b&gt;트랜잭션 B (사용자 B)&lt;/b&gt;가 새로운 사원(500,001번)을 등록하려고 시도한다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 트랜잭션 B 시작
INSERT INTO employees (emp_no, name) VALUES (500001, 'Lara');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Case 1: READ COMMITTED (넥스트 키 락 미사용)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트랜잭션 A는 500,000번 레코드에만 &lt;b&gt;레코드 락&lt;/b&gt;을 건다 (갭 락 없음)&lt;/li&gt;
&lt;li&gt;트랜잭션 B는 500,001번 데이터 INSERT에 &lt;b&gt;성공&lt;/b&gt;하고 COMMIT 한다&lt;/li&gt;
&lt;li&gt;트랜잭션 A가 다시 동일한 SELECT 쿼리를 실행하면, 아까는 없던 500,001번 'Lara'가 조회된다&lt;/li&gt;
&lt;/ol&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;h4 data-ke-size=&quot;size20&quot;&gt;Case 2: REPEATABLE READ (InnoDB 기본, 넥스트 키 락 사용)&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트랜잭션 A가 조회할 때, InnoDB는 &lt;b&gt;넥스트 키 락&lt;/b&gt;을 건다. 잠금 범위: 500,000번 레코드 + &lt;b&gt;500,000번 이후의 모든 간격(Gap)&lt;/b&gt; (무한대까지)&lt;/li&gt;
&lt;li&gt;트랜잭션 B가 &lt;code&gt;INSERT ... VALUES (500001, ...)&lt;/code&gt;을 시도한다&lt;/li&gt;
&lt;li&gt;하지만 500,000번 이후의 공간은 트랜잭션 A가 갭 락으로 막아두었으므로, 트랜잭션 B는 &lt;b&gt;대기 상태(Waiting)&lt;/b&gt;에 빠진다&lt;/li&gt;
&lt;li&gt;트랜잭션 A가 작업을 마치고 COMMIT 하거나 ROLLBACK 해야만 갭 락이 풀리고, 그때 비로소 트랜잭션 B의 INSERT가 실행된다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과: &lt;/b&gt;트랜잭션 A가 작업하는 동안에는 새로운 데이터가 끼어들 수 없으므로 팬텀 리드가 발생하지 않음.&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;6. MVCC와 락의 관계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MVCC가 없으면 SELECT도 항상 락을 잡아야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그렇다.&lt;/b&gt; MVCC(Multi-Version Concurrency Control) 기능이 없다면, SELECT 쿼리도 데이터의 일관성을 보장하기 위해 격리 수준에 따라 '잠금(Lock)'을 잡아야 한다.&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;MySQL InnoDB는 MVCC(언두 로그 활용) 덕분에 READ COMMITTED나 REPEATABLE READ에서도 &lt;b&gt;SELECT 작업 시 락을 전혀 걸지 않는다. &lt;/b&gt;누군가 데이터를 수정 중(UPDATE)이라도 대기하지 않고, &lt;b&gt;언두 로그에 있는 과거 버전의 데이터를 즉시 읽어온다.&lt;/b&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;다시 처음 질문으로 돌아가서,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;서로 다른 트랜잭션에서 겹치지 않는 FK 값을 가진 엔티티들을 동시에 삽입할 때 왜 데드락이 발생하는가?&quot;&lt;/p&gt;
&lt;/blockquote&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;b&gt;답: 값은 다르지만, 갭 락의 범위가 겹치기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니크/FK 제약조건이 있으면 INSERT 전에 중복 체크를 위해 해당 구간에 공유 잠금(S-Lock)을 건다. 이 S-Lock은 갭 락 형태로 걸리는데, 갭 락끼리는 호환되므로 여러 트랜잭션이 동시에 같은 구간을 잠글 수 있다. 문제는 실제 쓰기를 위해 X-Lock으로 업그레이드하려 할 때, 상대방의 S-Lock 때문에 서로 대기하게 되면서 데드락이 발생한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 외래 키나 유니크 인덱스에 의한 데드락이 빈번하게 발생하여 서비스에 지장을 주고 있다면, 격리 수준을 READ COMMITTED로 변경하는 것을 고려해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 격리 수준을 낮추면 &lt;b&gt;'팬텀 리드(Phantom Read)'&lt;/b&gt;나 '논 리피터블 리드(Non-Repeatable Read)' 현상이 발생할 수 있으므로, 데이터 정합성이 매우 중요한 집계성 쿼리나 금전 처리 로직에서는 주의가 필요하다. 일반적으로 웹 서비스 환경에서는 READ COMMITTED로도 충분한 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 애플리케이션 레벨에서 동시성을 제어하는 방법을 고려해볼 수 있다.&lt;/p&gt;</description>
      <category>개발 공부/DB</category>
      <category>lock</category>
      <category>MySQL</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/182</guid>
      <comments>https://gmelon.dev/182#entry182comment</comments>
      <pubDate>Mon, 2 Feb 2026 00:42:20 +0900</pubDate>
    </item>
    <item>
      <title>[Case Study] SLASH 22 - 왜 은행은 무한스크롤이 안 되나요</title>
      <link>https://gmelon.dev/180</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;일반 은행과 토스뱅크의 거래 내역 조회 방식 비교 및 시스템 아키텍처&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 은행 앱의 거래 내역 조회 방식의 불편함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 은행 앱은 거래 내역 조회 시 상단에 기간 설정을 요구한다. 지정된 기간만큼만 스크롤하여 조회할 수 있으며, 그 이상을 조회하려면 유저가 직접 조회 기간을 변경해야 한다. 반면, 트위터, 인스타그램, 카카오톡 등 많은 인터넷 서비스들은 기간 설정을 요구하지 않고, 스크롤만 계속하면 십수 년 전의 글까지 나열되는 우아한 스크롤 UI를 제공한다. 일반 은행이 이러한 불편한 인터페이스를 제공하는 이유는 시스템 아키텍처 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;은행 시스템의 일반적인 분리 구조 (채널계와 계정계)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4A4bU/dJMcaiIvOZE/DTl8ReMSAg2uzj3KfTyZlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4A4bU/dJMcaiIvOZE/DTl8ReMSAg2uzj3KfTyZlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4A4bU/dJMcaiIvOZE/DTl8ReMSAg2uzj3KfTyZlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4A4bU%2FdJMcaiIvOZE%2FDTl8ReMSAg2uzj3KfTyZlK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1712&quot; height=&quot;708&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;계정계 (Core Banking System)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 유저의 돈을 다루며 원본 데이터가 저장되는 영역이다. 장애나 오류 발생 시 치명적이므로 아주 높은 신뢰도가 요구된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;채널계 (Channel System)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 요청을 직접 받아 처리하는 영역이다. 돈을 직접 다루지는 않으며, 그런 요청은 계정계로 전달하여 처리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토스뱅크의 채널계와 계정계 아키텍처 특징&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rrD8m/dJMcacuNZmA/kjxOrXmJu8Mh9ZqxcwtaCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rrD8m/dJMcacuNZmA/kjxOrXmJu8Mh9ZqxcwtaCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rrD8m/dJMcacuNZmA/kjxOrXmJu8Mh9ZqxcwtaCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrrD8m%2FdJMcacuNZmA%2FkjxOrXmJu8Mh9ZqxcwtaCK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1712&quot; height=&quot;786&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;토스뱅크 채널계 아키텍처&lt;/h4&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;&lt;b&gt;장점:&lt;/b&gt; 특정 서버에 부하가 몰리면 스케일 아웃(확장)이 가능하고, DB 부하가 커지면 DB를 분리할 수 있어 큰 트래픽을 다루는 데 유리하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점:&lt;/b&gt; 네트워크 구조가 복잡하고 DB가 여러 개로 나뉘어 있어 경우에 따라 트랜잭션 처리가 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;토스뱅크 계정계 아키텍처&lt;/h4&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; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; 서버가 하나이므로 네트워크 구조가 단순하고, DB가 하나이므로 트랜잭션 처리가 유리하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점:&lt;/b&gt; 특정 서버만 스케일 아웃하거나 DB를 분리할 수 없어 급증하는 트래픽을 소화하기에 다소 불리하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선택 이유:&lt;/b&gt; 오류 없이 동작하는 것이 매우 중요하기 때문에 성능을 희생하더라도 높은 신뢰도를 위한 아키텍처를 택하게 된다.&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일반 은행의 기간 설정 이유와 토스뱅크의 무한 스크롤 구현 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 은행이 기간 설정을 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래 내역은 은행의 핵심 기록이므로 당연히 채널계가 아닌 계정계에서 관리한다. 채널계는 고객의 거래 내역 요청을 받아 계정계에 전달하고, 그 결과를 다시 앱으로 전달한다. 계정계는 성능보다 신뢰도가 우선이므로, 광범위한 거래 내역 조회를 빠른 응답 시간과 적은 부하로 제공하기 어렵다. 따라서 계정계에 대한 요청을 최소화하기 위해 UI에서 디폴트 조회 범위를 수개월 정도로 한정하는 것이 현명한 선택이다.&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;b&gt;채널계에 있는 송금 서버가 거래 내역을 직접 반환&lt;/b&gt;한다.&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;&amp;nbsp;&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;카프카를 활용한 거래 내역 동기화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카프카를 이용한 기본 동기화 구조 확립&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 컨셉 &amp;amp; 한계&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5UJE1/dJMcafZmwMv/rY6ylyxqWKNqqubNAdTRi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5UJE1/dJMcafZmwMv/rY6ylyxqWKNqqubNAdTRi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5UJE1/dJMcafZmwMv/rY6ylyxqWKNqqubNAdTRi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5UJE1%2FdJMcafZmwMv%2FrY6ylyxqWKNqqubNAdTRi0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1734&quot; height=&quot;752&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 이체를 실행할 때마다 이체 내역을 송금 서버 DB에 저장하는 방식은, 다른 은행 앱을 이용해 토스뱅크 계좌로 입금하는 경우를 처리할 수 없다. 따라서 코어 뱅킹 서버가 송금 서버에게 입금 사실을 알려주어야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;카프카(Kafka) 도입&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wz073/dJMcabCEZbX/KXffwqtiDIsDVr7UZEJft1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wz073/dJMcabCEZbX/KXffwqtiDIsDVr7UZEJft1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wz073/dJMcabCEZbX/KXffwqtiDIsDVr7UZEJft1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWz073%2FdJMcabCEZbX%2FKXffwqtiDIsDVr7UZEJft1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1734&quot; height=&quot;978&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;978&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스뱅크는 카프카를 이용하여 이 문제를 해결한다. 코어 뱅킹 서버가 토픽에 메시지를 프로듀싱(Producing)하고, 송금 서버가 이를 컨슈밍(Consuming)하여 송금 DB에 저장한다. 이로써 토스 앱을 이용하든 타행 앱을 이용하든, 코어 뱅킹 서버에 요청하지 않고도 토스뱅크의 모든 거래를 조회할 수 있게 된다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;기본적인 예외 처리 방안들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;송금 실행 중 타임아웃 및 중복 송금 방지 처리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;송금 실행 중 타임아웃 발생 시 처리&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z6tHP/dJMcagjFoRT/f9vGxhC59CGAlTWudJtXiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z6tHP/dJMcagjFoRT/f9vGxhC59CGAlTWudJtXiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z6tHP/dJMcagjFoRT/f9vGxhC59CGAlTWudJtXiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz6tHP%2FdJMcagjFoRT%2Ff9vGxhC59CGAlTWudJtXiK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1654&quot; height=&quot;796&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제:&lt;/b&gt; 송금 실행 중 코어 뱅킹 서버의 응답이 늦어져 타임아웃이 발생하면, 송금 서버는 응답을 받지 못해 거래 내역을 DB에 저장하지 못한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sSfaX/dJMcagjFoRV/YJQgbijjTkks5qoahSEqdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sSfaX/dJMcagjFoRV/YJQgbijjTkks5qoahSEqdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sSfaX/dJMcagjFoRV/YJQgbijjTkks5qoahSEqdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsSfaX%2FdJMcagjFoRV%2FYJQgbijjTkks5qoahSEqdK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1678&quot; height=&quot;746&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; 타임아웃 발생 후 코어 뱅킹 서버가 카프카 토픽을 통해 송금 서버에게 이체 실행 결과를 알려주면, 송금 서버는 거래 내역을 DB에 저장하고 유저에게 조회해 줄 수 있다. 이 경우 송금 직후에는 난감할 수 있으나, 최종적으로 송금이 완료되면 거래 내역에 성공 거래로 남게 된다.&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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;782&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHpRaA/dJMcabimFjp/V9nUdRMqxiIHcqs7NiHBrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHpRaA/dJMcabimFjp/V9nUdRMqxiIHcqs7NiHBrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHpRaA/dJMcabimFjp/V9nUdRMqxiIHcqs7NiHBrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHpRaA%2FdJMcabimFjp%2FV9nUdRMqxiIHcqs7NiHBrk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1696&quot; height=&quot;782&quot; data-origin-width=&quot;1696&quot; data-origin-height=&quot;782&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&lt;b&gt;해결:&lt;/b&gt; 송금 요청이 들어오면, 송금 서버는 코어 뱅킹 서버에 요청을 보내기 전에 우선 송금 요청을 DB에 저장한다. 완료되지 않은 송금 요청이 있는 유저가 다시 송금을 요청하면 거절한다. 나중에 송금이 완료되어 송금 서버가 거래 내역 저장을 끝내고 나면, 그때부터 새로운 송금 요청을 수락한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;송금 요청 영구 지연 문제 해결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제:&lt;/b&gt; 네트워크 문제 등으로 송금 서버가 보낸 요청이 코어 뱅킹 서버에 도달하지 못하면, 송금 요청이 영원히 진행 중인 상태로 송금 DB에 남아 유저가 영원히 송금을 할 수 없게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; 송금 서버는 주기적으로 코어 뱅킹 서버에게 송금 요청의 상태를 확인한다. 코어 뱅킹 서버는 해당 송금 건에 대해 전혀 모르므로, 송금 요청이 없었다고 응답할 것이다. 송금 서버는 요청 전달에 실패한 것으로 보고 송금 실패로 처리하며, 유저는 계속해서 송금을 할 수 있게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성공 거래의 실패 처리 가능성 방지 (타임아웃 약속)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;희박한 실패 처리 오류 가능성&lt;/h4&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;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;타임아웃 약속을 통한 해결&lt;/h4&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;송금 서버와 코어 뱅킹 서버는 타임아웃 시간을 1분으로 약속하고, 송금 서버는 요청 시각을 포함하여 보낸다. 타임아웃 시간이 지났는데 코어 뱅킹 서버로부터 완료 알림이 오지 않으면, 송금 서버는 상태를 물어본 뒤 '없음' 응답이 오면 실패로 처리한다.&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;거래 내역 동기화 중 발생하는 데이터 누락 및 지연 문제 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;거래 내역 저장 실패 시 재시도 및 Dead Letter 처리&lt;/h3&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;b&gt;해결:&lt;/b&gt; 송금 서버는 재시도 후에 송금 완료 메시지를 다시 컨슈머하고 이력을 저장한다. 재시도 후에도 계속 실패하는 경우, 더 이상 재시도하지 않고 &lt;b&gt;컨슈머 데드 레터(Consumer Dead Letter)&lt;/b&gt; 카프카 토픽에 실패한 메시지를 저장한다. &lt;b&gt;개발자가 실패 원인을 확인하고 문제를 해결한 뒤&lt;/b&gt;, 해당 토픽을 다시 컨슈머하여 송금 이력을 저장하게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;과거 거래 내역 누락 가능성 방지&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1692&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sQaFN/dJMcaaX3CmN/EJFtcqILM8jCUZz4o8WLGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sQaFN/dJMcaaX3CmN/EJFtcqILM8jCUZz4o8WLGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sQaFN/dJMcaaX3CmN/EJFtcqILM8jCUZz4o8WLGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsQaFN%2FdJMcaaX3CmN%2FEJFtcqILM8jCUZz4o8WLGK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1692&quot; height=&quot;670&quot; data-origin-width=&quot;1692&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제:&lt;/b&gt; 500원 입금 동기화가 실패하고 100원 출금 동기화가 성공한 뒤, 500원 입금 재동기화가 성공하기 전에 유저가 거래 내역을 조회하면, 잔액이 -100원이 되는 이상한 상황을 겪게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O74Kt/dJMcaiBJQn9/MZRR5CpA3iRUPYQJ6UHJK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O74Kt/dJMcaiBJQn9/MZRR5CpA3iRUPYQJ6UHJK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O74Kt/dJMcaiBJQn9/MZRR5CpA3iRUPYQJ6UHJK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO74Kt%2FdJMcaiBJQn9%2FMZRR5CpA3iRUPYQJ6UHJK0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1638&quot; height=&quot;688&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&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;따라서 100원 출금 건을 동기화할 때 500원 입금이 누락되었음을 발견하고, 500원 입금을 먼저 동기화한 후 100원 출금 건을 동기화하여 과거 거래 내역 누락을 방지한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;누락된 거래 동기화 실패 시 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 누락된 500원 입금 동기화가 또다시 실패한다면, 100원 출금 건도 동기화하지 않는다. 이는 순간적으로 잔액이 -100원이 되는 문제를 피하기 위함이다. 이 카프카 메시지 컨슘은 실패로 처리되므로, 송금 서버는 다시 컨슈머하여 동기화를 시도할 것이다.&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;b&gt;문제:&lt;/b&gt; 동기화가 완료되기 전에 유저가 거래 내역 조회를 하면, 입금된 500원과 출금된 100원이 모두 보이지 않는 문제가 발생한다. 유저는 방금 성공한 출금 내역이 보이지 않으면 당황할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R7twy/dJMcagjFoWi/mISmo1S4sbHLxDKW8XW1qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R7twy/dJMcagjFoWi/mISmo1S4sbHLxDKW8XW1qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R7twy/dJMcagjFoWi/mISmo1S4sbHLxDKW8XW1qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR7twy%2FdJMcagjFoWi%2FmISmo1S4sbHLxDKW8XW1qk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1662&quot; height=&quot;688&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; 송금 서버는 진행 중인 거래 내역을 저장하고 있다. 유저가 거래 내역을 조회하면, 송금 서버는 현재 진행 중인 100원 출금 항목을 발견하고 동기화를 시도한다. 이때 500원 입금도 아직 동기화되지 않은 상태이므로, 100원 출금보다 먼저 동기화하여 모든 거래가 잘 동기화된 정상적인 거래 내역을 유저에게 보여준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대규모 트래픽 환경에서의 동기화 지연 방지 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대규모 동기화 지연 문제가 발생하는 상황 예시&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYkvf5/dJMcajm6HIz/TUb5AKKMHnO8Kby9iaHJ1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYkvf5/dJMcajm6HIz/TUb5AKKMHnO8Kby9iaHJ1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYkvf5/dJMcajm6HIz/TUb5AKKMHnO8Kby9iaHJ1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYkvf5%2FdJMcajm6HIz%2FTUb5AKKMHnO8Kby9iaHJ1k%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1856&quot; height=&quot;740&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 고객에게 100원씩 입금하는 이벤트로 100만 건의 거래 내역을 동기화해야 하는 경우를 가정한다. 1시간에 걸쳐 입금이 완료된다면 1초에 약 300건씩 처리될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 한 건 처리에 100밀리초(ms)가 걸린다면, 1초에 10건, 즉 10만 개의 동기화가 일어나므로 1초에 300건씩 들어오는 처리량을 다 처리하지 못하고 점점 밀리게 된다. 100만 건이 밀리면 이를 모두 처리하는 데 28시간이 걸리므로, 그동안 새로운 입금 거래가 동기화되기 전까지 유저에게 보이지 않는 문제가 생긴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카프카 파티셔닝을 통한 동시성 확보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 메시지들을 여러 파티션으로 나누어 여러 개의 컨슈머가 처리할 수 있게 해준다. 이때 유저가 지정한 키(Key)를 기준으로 파티션을 나누어 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래 내역 동기화의 경우 계좌 번호를 키로 사용하여 같은 계좌의 거래 내역은 반드시 같은 파티션에 들어가게 된다. 같은 파티션은 같은 컨슈머가 처리하므로, 여러 컨슈머가 같은 계좌를 동기화하면서 발생할 수 있는 동시성 문제를 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초당 300건의 입금 요청을 처리해야 하는 경우, 파티션이 30개라면 충분히 처리할 수 있을 것으로 추정된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;워커 스레드 활용을 통한 처리량 극대화&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파티션 무한 증설의 한계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 계속 늘어나 더 높은 처리량이 요구되더라도 파티션을 무한적으로 늘릴 수 없다. 파티션을 늘리는 데 시간이 걸려 유저가 동기화 지연 문제를 겪게 될 수 있다. 피크치를 기준으로 넉넉하게 파티션을 잡으면 시스템 자원을 차지하고, 한 번 늘린 파티션은 다시 줄일 수 없어 지속적인 자원 낭비가 될 수 있다.&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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9SFc7/dJMb995VjiR/g5J4QXuz3QvJq5g97qyaf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9SFc7/dJMb995VjiR/g5J4QXuz3QvJq5g97qyaf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9SFc7/dJMb995VjiR/g5J4QXuz3QvJq5g97qyaf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9SFc7%2FdJMb995VjiR%2Fg5J4QXuz3QvJq5g97qyaf1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1740&quot; height=&quot;718&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 개수는 적당한 수준을 유지하고, 대신 컨슈머별로 워커 스레드를 충분히 할당한다. 예를 들어, 파티션을 10개로 하고 워커 스레드를 100개로 한다면, 총 1,000개의 스레드로 동기화가 가능해져 초당 1만 건의 동기화가 가능해진다. (건당 100ms 소요 기준)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;워커 스레드 동시성 문제 방지&lt;/h4&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최후의 수단: 유저 수동 동기화 버튼 제공&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://toss.im/slash-22/sessions/2-5&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://toss.im/slash-22/sessions/2-5&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발 공부/기타</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/180</guid>
      <comments>https://gmelon.dev/180#entry180comment</comments>
      <pubDate>Mon, 1 Dec 2025 01:38:35 +0900</pubDate>
    </item>
    <item>
      <title>엄청 커다란 파일 다운로드</title>
      <link>https://gmelon.dev/179</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;만약 서버 &amp;rarr; 클라이언트로 큰 크기의 파일을 생성해서 반환해야 된다고 생각해보자. 예를 들어 전체 유저에 대한 통계 데이터를 생성 + 압축해서 클라이언트에게 반환해야 한다고 해보자. 만약 완성된 전체 데이터의 크기가 3GB 라고 하면 메모리가 3GB 이하인 (혹은 그 이상이어도) 서버는 버티지 못하고 OOM 으로 죽어버릴 것이다. 또한 통계성으로 유저에 대한 이런저런 정보를 모두 모아야 해서, 생성하는데 시간이 오래걸리는 데이터라면 timeout 도 발생할 가능성이 있다. 이러한 문제를 어떻게 해결하면 좋을까??&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_ykomsdykomsdykom.png&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dm9hJ/dJMcadf5np7/XkpuI8rAXgcckeBEGpDsxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dm9hJ/dJMcadf5np7/XkpuI8rAXgcckeBEGpDsxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dm9hJ/dJMcadf5np7/XkpuI8rAXgcckeBEGpDsxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDm9hJ%2FdJMcadf5np7%2FXkpuI8rAXgcckeBEGpDsxk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2816&quot; height=&quot;1536&quot; data-filename=&quot;Gemini_Generated_Image_ykomsdykomsdykom.png&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 스트리밍 다운로드&lt;/h2&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. `.zip` 으로 압축&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;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 스트리밍으로 바로 클라에게 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 모든 유저에 대해 `1~2` 반복 (이전 유저에 대한 데이터는 GC 가능)&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;pre id=&quot;code_1763902061288&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/download&quot;)
@Slf4j
public class DownloadTestController {

    /**
     * 스트리밍 방식 - 1000명 유저 데이터를 ZIP으로 압축하면서 즉시 전송
     */
    @GetMapping(&quot;/streaming&quot;)
    public ResponseEntity&amp;lt;StreamingResponseBody&amp;gt; downloadStreaming() {

        log.info(&quot;=== 스트리밍 다운로드 시작 ===&quot;);
        long startMemory = getMemoryUsage();
        long startTime = System.currentTimeMillis();

        log.info(&quot;시작 메모리: {}MB&quot;, startMemory);

        StreamingResponseBody stream = outputStream -&amp;gt; {
            try (ZipOutputStream zipOut = new ZipOutputStream(outputStream)) {

                zipOut.setLevel(Deflater.BEST_SPEED);

                int userCount = 1000;

                for (int i = 1; i &amp;lt;= userCount; i++) {
                    String userId = String.format(&quot;USER_%04d&quot;, i);

                    // ZIP 엔트리 추가
                    ZipEntry entry = new ZipEntry(userId + &quot;.dat&quot;);
                    zipOut.putNextEntry(entry);

                    // 2MB 데이터 생성하면서 즉시 압축
                    byte[] buffer = new byte[2 * 1024 * 1024];
                    new Random().nextBytes(buffer);
                    zipOut.write(buffer);  // 즉시 압축되고 전송됨

                    zipOut.closeEntry();

                    if (i % 100 == 0) {
                        long currentMemory = getMemoryUsage();
                        log.info(&quot;진행: {}/{}명, 현재 메모리: {}MB, 증가량: {}MB&quot;, i, userCount, currentMemory, currentMemory - startMemory);
                    }
                }

                long endTime = System.currentTimeMillis();
                long endMemory = getMemoryUsage();

                log.info(&quot;=== 스트리밍 완료 ===&quot;);
                log.info(&quot;소요시간: {}초&quot;, (endTime - startTime) / 1000);
                log.info(&quot;시작 메모리: {}MB&quot;, startMemory);
                log.info(&quot;종료 메모리: {}MB&quot;, endMemory);
                log.info(&quot;메모리 증가: {}MB&quot;, endMemory - startMemory);
            }
        };

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        &quot;attachment; filename=\&quot;users-streaming.zip\&quot;&quot;)
                .contentType(MediaType.parseMediaType(&quot;application/zip&quot;))
                .body(stream);
    }

    /**
     * 비스트리밍 방식 - 1000명 유저 데이터를 전부 메모리에서 ZIP으로 압축 후 한번에 반환
     */
    @GetMapping(&quot;/non-streaming&quot;)
    public ResponseEntity&amp;lt;byte[]&amp;gt; downloadNonStreaming() {

        log.info(&quot;=== 비스트리밍 다운로드 시작 ===&quot;);
        long startMemory = getMemoryUsage();
        long startTime = System.currentTimeMillis();

        log.info(&quot;시작 메모리: {}MB&quot;, startMemory);

        try {
            // 메모리에 전체 ZIP 생성
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {

                zipOut.setLevel(Deflater.BEST_SPEED);

                int userCount = 1000;

                for (int i = 1; i &amp;lt;= userCount; i++) {
                    String userId = String.format(&quot;USER_%04d&quot;, i);

                    // ZIP 엔트리 추가
                    ZipEntry entry = new ZipEntry(userId + &quot;.dat&quot;);
                    zipOut.putNextEntry(entry);

                    // 2MB 데이터 생성
                    byte[] buffer = new byte[2 * 1024 * 1024];
                    new Random().nextBytes(buffer);
                    zipOut.write(buffer);  // 메모리에 계속 쌓임

                    zipOut.closeEntry();

                    if (i % 100 == 0) {
                        long currentMemory = getMemoryUsage();
                        log.info(&quot;진행: {}/{}명, 현재 메모리: {}MB, 증가량: {}MB&quot;, i, userCount, currentMemory, currentMemory - startMemory);
                    }
                }
            }

            // 완성된 ZIP을 byte[]로 변환
            byte[] zipData = baos.toByteArray();

            long endTime = System.currentTimeMillis();
            long endMemory = getMemoryUsage();

            log.info(&quot;=== 비스트리밍 완료 ===&quot;);
            log.info(&quot;ZIP 크기: {}MB&quot;, zipData.length / 1024 / 1024);
            log.info(&quot;소요시간: {}초&quot;, (endTime - startTime) / 1000);
            log.info(&quot;시작 메모리: {}MB&quot;, startMemory);
            log.info(&quot;종료 메모리: {}MB&quot;, endMemory);
            log.info(&quot;메모리 증가: {}MB&quot;, endMemory - startMemory);

            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION,
                            &quot;attachment; filename=\&quot;users-non-streaming.zip\&quot;&quot;)
                    .contentType(MediaType.parseMediaType(&quot;application/zip&quot;))
                    .body(zipData);

        } catch (OutOfMemoryError | IOException e) {
            long errorMemory = getMemoryUsage();
            log.error(&quot;OutOfMemoryError 발생! 현재 메모리: {}MB&quot;, errorMemory);

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    private long getMemoryUsage() {
        Runtime runtime = Runtime.getRuntime();
        return (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
    }
}&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;/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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU09Hu/dJMcafZjZnm/b4oZ2Pxkphxd63qkE0g9tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU09Hu/dJMcafZjZnm/b4oZ2Pxkphxd63qkE0g9tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU09Hu/dJMcafZjZnm/b4oZ2Pxkphxd63qkE0g9tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU09Hu%2FdJMcafZjZnm%2Fb4oZ2Pxkphxd63qkE0g9tK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;141&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;222&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;메모리 사용량 변화를 보면, 조금씩 변화는 있지만 대략 300MB 를 넘지 않는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(GC 나 메모리 설정에 따라 점진적으로 증가하다가 GC 가 되어 확 줄어드는 모습이 반복되는 케이스도 있었음)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwetMn/dJMcacVPv9Z/nxreqALyDTpWKlDeu7W2gK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwetMn/dJMcacVPv9Z/nxreqALyDTpWKlDeu7W2gK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwetMn/dJMcacVPv9Z/nxreqALyDTpWKlDeu7W2gK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwetMn%2FdJMcacVPv9Z%2FnxreqALyDTpWKlDeu7W2gK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;389&quot; height=&quot;780&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5upDc/dJMcad1seI2/oK2lpWk0Jwa3kGON0pV191/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5upDc/dJMcad1seI2/oK2lpWk0Jwa3kGON0pV191/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5upDc/dJMcad1seI2/oK2lpWk0Jwa3kGON0pV191/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5upDc%2FdJMcad1seI2%2FoK2lpWk0Jwa3kGON0pV191%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;398&quot; height=&quot;387&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;826&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;만약 JVM의 메모리 제한이 2GB 였다면, 파일을 클라이언트로 반환하다가 중간에 터졌을 것이다. 중간에 한번 GC 가 되긴 했지만 어쨌든 다운로드 받아야 하는 `.zip` 파일 자체는 메모리에 계속 올려두고 있어야 하므로 스트리밍과 같은 효율적인 GC 는 불가능할 것으로 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccF7zf/dJMcaf55INt/FuFrgDSB3sF4TJjmRryeOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccF7zf/dJMcaf55INt/FuFrgDSB3sF4TJjmRryeOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccF7zf/dJMcaf55INt/FuFrgDSB3sF4TJjmRryeOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccF7zf%2FdJMcaf55INt%2FFuFrgDSB3sF4TJjmRryeOk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;432&quot; height=&quot;209&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. S3 에 스트리밍으로 업로드 + 클라에는 Presigned URL 만 전달&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트리밍 방식으로 메모리 제약은 극복했지만, timeout 제약은 여전히 극복이 불가능하다. 즉, 통계 데이터 계산에 시간이 오래걸려 설정된 tomcat / aws 등의 timeout 을 초과하게 되면 여전히 클라이언트는 파일을 정상적으로 받을 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SDWzc/dJMcaiuVXZI/vHCBLc6pCu4DAYjagYFEa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SDWzc/dJMcaiuVXZI/vHCBLc6pCu4DAYjagYFEa0/img.png&quot; data-alt=&quot;파일 다운로드 도중 timeout exception 이 발생하는 예제 케이스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SDWzc/dJMcaiuVXZI/vHCBLc6pCu4DAYjagYFEa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSDWzc%2FdJMcaiuVXZI%2FvHCBLc6pCu4DAYjagYFEa0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;456&quot; height=&quot;180&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파일 다운로드 도중 timeout exception 이 발생하는 예제 케이스&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;이런 경우 스트리밍 반환을 클라에게 하지 말고 S3 등의 저장소에 하는 방법이 가능할 것 같다.&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;1. 클라가 서버에게 파일 다운로드 요청을 보낸다.&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;3. 서버는 데이터를 생성하면서, S3 에 스트리밍 방식으로 데이터를 업로드 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 업로드가 완료되면 앞서 저장해둔 요청 정보와 S3 에 저장된 데이터를 연결해주고, 요청의 상태를 완료로 변경한다. (sse 나, 롱 폴링, 이메일 등으로 완료 여부를 클라에게 전달)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 클라에서 작업이 완료됨을 인지하면 서버에게 presigned url 을 요청하고, 서버가 반환해준 url 을 통해 파일을 다운로드 받는다.&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;요청을 저장하는 방법은 여러 가지가 있겠지만, 만약 단순히 db 에 저장한다고 하면 아래와 같은 엔티티를 사용할 수 있을 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1763908569758&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DownloadRequest {

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

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;

    @Enumerated(EnumType.STRING)
    private DownloadType type;

    @Enumerated(EnumType.STRING)
    private DownloadStatus status;
    
    // s3 관련 데이터들 (필요하면 테이블 분리도 가능)
    private String fileName;
    
    pricate String objectKey;
    
    // createdAt, updatedAt 정보들

    public enum DownloadType {
        STATISTICS
    }

    public enum DownloadStatus {
        PENDING,
        PROCESSING,
        COMPLETED,
        FAILED
    }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라에서 처음에 요청을 보내면 DownloadStatus 가 PENDING 으로 생성이 되고, 작업이 시작되면 PROCESSING 으로 변경된 후 완료가 되면 COMPLETED 나 FAILED 로 변경되는 식이다.&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;완료된 요청을 확인하고 클라에서 서버에게 파일을 요청하면 서버는 objectKey 를 통해 파일에 대한 Presigned URL 를 생성해 클라에게 전달할 수 있다. 또한 원한다면 스트리밍 방식으로 S3 -&amp;gt; 서버 -&amp;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;</description>
      <category>개발 공부/Spring</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/179</guid>
      <comments>https://gmelon.dev/179#entry179comment</comments>
      <pubDate>Sun, 23 Nov 2025 23:36:20 +0900</pubDate>
    </item>
    <item>
      <title>2024 상반기 회고</title>
      <link>https://gmelon.dev/166</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;끝내주는 회고를 쓰고 싶어 계속 미루고 있었지만 역시나 미뤄도 퀄리티는 크게 달라지지 않기에, 무작정 써보기로 합니다. 무엇보다&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;블루멜론이 될 순 없기 때문에 어서 2024 상반기 회고를 써보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFJsE2/btsI00h5gUE/4bSDdigokXMYOr4UIsmgGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFJsE2/btsI00h5gUE/4bSDdigokXMYOr4UIsmgGK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;708&quot; data-filename=&quot;KakaoTalk_Snapshot_20240811_163458.png&quot; style=&quot;width: 31.1215%; margin-right: 10px;&quot; data-widthpercent=&quot;31.49&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFJsE2/btsI00h5gUE/4bSDdigokXMYOr4UIsmgGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFJsE2%2FbtsI00h5gUE%2F4bSDdigokXMYOr4UIsmgGK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMgRaE/btsI1pu9cCP/KwVgQb5etIMnsUShM7TW00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMgRaE/btsI1pu9cCP/KwVgQb5etIMnsUShM7TW00/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;322&quot; data-filename=&quot;KakaoTalk_Snapshot_20240811_163433.png&quot; width=&quot;439&quot; height=&quot;186&quot; data-widthpercent=&quot;68.51&quot; style=&quot;width: 67.7158%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMgRaE/btsI1pu9cCP/KwVgQb5etIMnsUShM7TW00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMgRaE%2FbtsI1pu9cCP%2FKwVgQb5etIMnsUShM7TW00%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;아 아직 블루멜론 아니라고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ln1O3/btsI1W0rs06/A3iCtIPqp7iirelv8ZTB2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ln1O3/btsI1W0rs06/A3iCtIPqp7iirelv8ZTB2K/img.png&quot; data-alt=&quot;뭔가 기시감이 들어 찾아보니 작년에도 같은 말을.. 했었네&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ln1O3/btsI1W0rs06/A3iCtIPqp7iirelv8ZTB2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLn1O3%2FbtsI1W0rs06%2FA3iCtIPqp7iirelv8ZTB2K%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1278&quot; height=&quot;100&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뭔가 기시감이 들어 찾아보니 작년에도 같은 말을.. 했었네&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싸피&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 올해 상반기를 관통하는 가장 큰 키워드다. 1월에 싸피 11기 대전 캠퍼스에 입과했고 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;아래처럼 노션에 내용을 정리하면서 공부했다. 각 페이지 별로 내용도 꽤 많고 개수도 70개가 넘는 것을 보고 그래도 열심히 공부했구나 하는 뿌듯함이 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2412&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci7vcg/btsI2hXzViS/CNPmv4680OFbaggla7Z0V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci7vcg/btsI2hXzViS/CNPmv4680OFbaggla7Z0V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci7vcg/btsI2hXzViS/CNPmv4680OFbaggla7Z0V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci7vcg%2FbtsI2hXzViS%2FCNPmv4680OFbaggla7Z0V1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2412&quot; height=&quot;1294&quot; data-origin-width=&quot;2412&quot; data-origin-height=&quot;1294&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lFlD0/btsI0ReGq6Q/MbXIZWOxFyx6LK5AjfefHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lFlD0/btsI0ReGq6Q/MbXIZWOxFyx6LK5AjfefHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lFlD0/btsI0ReGq6Q/MbXIZWOxFyx6LK5AjfefHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlFlD0%2FbtsI0ReGq6Q%2FMbXIZWOxFyx6LK5AjfefHk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1240&quot; height=&quot;182&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업을 들으면서 헷갈렸던 내용들을 모두 메모해 뒀다가 수업이 끝나고 선생님께 질문드렸다. 언젠가 한 번은 선생님께서 '상혁님 질문은 답변하려면 나도 찾아보고 고민해봐야 하네요. 이런 질문 많이 해줘서 고마워요! 앞으로 더 많이 해주세요.'라는 피드백을 주신 적이 있었는데 내가 올바른 방향으로 학습하고 고민하고 있구나라는 뿌듯함과 함께 나도 저런 말을 할 수 있는 사람이 되어야겠다는 존경 어린 다짐을 하게 되었다. 1학기 때 우리 반을 맡아주셨던 선생님은 지금은 다른 지역 캠퍼스에 가셨는데 다시 한번 감사했다고 여기서나마 외쳐봅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싸피 스터디&lt;/h3&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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daxtSQ/btsI0vwu9L4/Pf95PwgR809vCA5mGBXXU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daxtSQ/btsI0vwu9L4/Pf95PwgR809vCA5mGBXXU1/img.png&quot; data-alt=&quot;뽀짝&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daxtSQ/btsI0vwu9L4/Pf95PwgR809vCA5mGBXXU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaxtSQ%2FbtsI0vwu9L4%2FPf95PwgR809vCA5mGBXXU1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2856&quot; height=&quot;2160&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;2160&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;자바와 스프링에 대한 이해도를 높이고 개발에 적용해 보자! 하는 게 스터디의 목표였고, 토비의 스프링을 읽고 레포에 질문을 각자 올린 후 모여서 토론하는 방식으로 진행했다. 스터디원 모두가 각자 다른 장점과 역량을 가지고 있었기 때문에 시너지를 내면서 함께 성장할 수 있었다고 생각한다. 부족한 나를 이끌어준 스터디원들에게 정말정말 고마웠던 6개월이었다. 1학기 막바지에 프로젝트로 일정이 바빠 힘든 상황에서도 어떻게든 진도를 나가보자고 노력한 덕분에 얼마 전 목표했던 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;사실 실습은 하지 않고 건너뛴 부분도 많고 후반부 AOP 부분은 깊게 이해하지 못하고 넘어간 내용도 많았다. 하지만 책을 읽으며 스프링이 어떤 가치를 지향하는지, 그걸 달성하기 위해 어떤 설계를 적용하고 있는지 깨닫는 순간들이 정말 신기하고 즐거웠다. 초반에는 책을 읽으며 계속 와,, 와,,,? 아 이게 이래서..? 의 연속이었다. 그만큼 많은 깨달음이 있었고 무엇을/왜 공부해야 하는지에 대한 감이 조금 잡혔다고 느꼈다.&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;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQyCht/btsI012rZAn/59s0jPspiRhR6Vxkd58KHk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQyCht/btsI012rZAn/59s0jPspiRhR6Vxkd58KHk/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1813&quot; data-filename=&quot;KakaoTalk_Photo_2024-08-12-01-03-00.jpeg&quot; style=&quot;width: 18.0692%; margin-right: 10px;&quot; data-widthpercent=&quot;18.28&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQyCht/btsI012rZAn/59s0jPspiRhR6Vxkd58KHk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQyCht%2FbtsI012rZAn%2F59s0jPspiRhR6Vxkd58KHk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1170&quot; height=&quot;1813&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FJGrE/btsI1u4bHee/GWFpK5DJMbr84LH353kTT1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FJGrE/btsI1u4bHee/GWFpK5DJMbr84LH353kTT1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;104&quot; data-filename=&quot;KakaoTalk_Photo_2024-08-12-01-03-05.jpeg&quot; style=&quot;width: 80.768%;&quot; data-widthpercent=&quot;81.72&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FJGrE/btsI1u4bHee/GWFpK5DJMbr84LH353kTT1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFJGrE%2FbtsI1u4bHee%2FGWFpK5DJMbr84LH353kTT1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;이걸 제가 찾아넀습니다. 모자이크가 필요없는 광기. 와 우리 티비 나왔다~~&lt;/figcaption&gt;
&lt;/figure&gt;
&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3126&quot; data-origin-height=&quot;2332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNSlja/btsI1mkYUcq/w97ME7V5M5WayCtRwrCYlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNSlja/btsI1mkYUcq/w97ME7V5M5WayCtRwrCYlK/img.png&quot; data-alt=&quot;누가 싸피 하는건 좀 어때? 라고 물어볼 때 마다 빼놓지 않고 보여주는 (얘들아 미안) 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNSlja/btsI1mkYUcq/w97ME7V5M5WayCtRwrCYlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNSlja%2FbtsI1mkYUcq%2Fw97ME7V5M5WayCtRwrCYlK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;3126&quot; height=&quot;2332&quot; data-origin-width=&quot;3126&quot; data-origin-height=&quot;2332&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qpBuW/btsI1EyVekA/aIOGpeC8lUxSKdWEPy1ng0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qpBuW/btsI1EyVekA/aIOGpeC8lUxSKdWEPy1ng0/img.png&quot; data-alt=&quot;싸피 각자 러닝이라는 런데이 크루도 만들었다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qpBuW/btsI1EyVekA/aIOGpeC8lUxSKdWEPy1ng0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqpBuW%2FbtsI1EyVekA%2FaIOGpeC8lUxSKdWEPy1ng0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1170&quot; height=&quot;1165&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1165&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;졸업 후 싸피 입과 전에 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;h3 data-ke-size=&quot;size23&quot;&gt;취업 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1학기 과정을 들으면서 동시에 취업 준비도 계속했다. 최대한 지원서를 많이 써보려고 노력했다. 그럼에도 자소설닷컴에서 미제출로 넘겨버린 카드가 수도 없이 많지만,, 서너 군데 기업의 최종 면접 기회를 얻을 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sZ0vt/btsI0HXNyDr/VN3nonI0bjkdx6HCJnKvF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sZ0vt/btsI0HXNyDr/VN3nonI0bjkdx6HCJnKvF1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1585&quot; data-filename=&quot;edited_edited_IMG_4490.PNG&quot; data-widthpercent=&quot;55.69&quot; style=&quot;width: 55.0416%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sZ0vt/btsI0HXNyDr/VN3nonI0bjkdx6HCJnKvF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsZ0vt%2FbtsI0HXNyDr%2FVN3nonI0bjkdx6HCJnKvF1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1170&quot; height=&quot;1585&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWIxRl/btsI1w8LdYv/getWHkKjn3PguEOhvnzM11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWIxRl/btsI1w8LdYv/getWHkKjn3PguEOhvnzM11/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1992&quot; data-filename=&quot;edited_IMG_4689.PNG&quot; data-widthpercent=&quot;44.31&quot; style=&quot;width: 43.7956%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWIxRl/btsI1w8LdYv/getWHkKjn3PguEOhvnzM11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWIxRl%2FbtsI1w8LdYv%2FgetWHkKjn3PguEOhvnzM11%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1170&quot; height=&quot;1992&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnO9mo/btsI1lGolN0/ZlglkQPZJ8WFVOfBVlwuc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnO9mo/btsI1lGolN0/ZlglkQPZJ8WFVOfBVlwuc0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;536&quot; data-filename=&quot;무제.png&quot; style=&quot;width: 76.4835%; margin-right: 10px; margin-top: 10px;&quot; data-widthpercent=&quot;77.38&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnO9mo/btsI1lGolN0/ZlglkQPZJ8WFVOfBVlwuc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnO9mo%2FbtsI1lGolN0%2FZlglkQPZJ8WFVOfBVlwuc0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1318&quot; height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFOqkc/btsI1njT4Uo/lu9Dw3U9tYDBFIYArz4uHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFOqkc/btsI1njT4Uo/lu9Dw3U9tYDBFIYArz4uHK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1628&quot; data-filename=&quot;edited_IMG_5431.PNG&quot; data-widthpercent=&quot;22.62&quot; style=&quot;width: 22.3537%; margin-top: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFOqkc/btsI1njT4Uo/lu9Dw3U9tYDBFIYArz4uHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFOqkc%2FbtsI1njT4Uo%2Flu9Dw3U9tYDBFIYArz4uHK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1170&quot; height=&quot;1628&quot;/&gt;&lt;/span&gt;&lt;/div&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;하지만 싸피에서 수강한 알고리즘 커리큘럼과 수업 덕분에 &lt;a href=&quot;https://sh-hyun.tistory.com/162&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;삼성 SW 역량 테스트 B형을 취득&lt;/a&gt;할 수 있을 정도로 역량을 빠르게 늘릴 수 있었고 그 덕분인지 지원한 회사의 개수 대비 많은 면접 기회를 얻을 수 있었(&lt;s&gt;지만 다 말아먹었다&lt;/s&gt;)다. 면접 경험이 조금씩 쌓이면서 면접을 보는 스킬이나 태도도 조금씩 좋아지고 있다고 느꼈다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싸탈과 취업, 그리고 앞으로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 6월 말, 한창 싸피 2학기 준비를 하고 있는 와중에 싸피 잡페어를 통해 면접을 본 회사에서 최종 합격 연락을 받게 되었다. 싸피 2학기에 대한 기대가 컸기에 어떻게 하는 게 좋을지 고민이 많이 되었지만 경력을 쌓으면서 또 다른 방향의 성장을 해나갈 수 있을 것이라는 기대 반 두려움 반을 안고 입사를 결정했다. 입사 여부를 회신해야 하는 마감날짜가 2학기 시작 하루 전인가 그랬어서 결정 후 사무국에 바로 전화를 드려 퇴소 절차를 진행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caMly7/btsI063NkRp/5PaSkRQnG4W0uK3Vvytoe1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caMly7/btsI063NkRp/5PaSkRQnG4W0uK3Vvytoe1/img.jpg&quot; data-alt=&quot;고마운 스터디 친구들,,ㅜ 다시 보니 눈물나네요 보고싶습니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caMly7/btsI063NkRp/5PaSkRQnG4W0uK3Vvytoe1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaMly7%2FbtsI063NkRp%2F5PaSkRQnG4W0uK3Vvytoe1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;2048&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;2048&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;싸탈 얘기가 나온 김에 조금만 자랑 타임을 갖자면,, 싸피 1학기 동안 열심히 공부한 결과 반 1등으로 수료할 수 있었다. 싸피는 현금으로 교환이 가능한 마일리지 제도를 운영하고 있는데, 알고리즘&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;B형 취득으로 받은 마일리지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;100만 원과&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;성적&amp;nbsp;&lt;/span&gt;1등으로 받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;50만 원,&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기자단 활동으로 받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;30만 원을&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;탈탈 털어 일본 여행도 다녀왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nb0Z6/btsI1wOtajm/UmyBpY9Ka9TlyC7r33qqP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nb0Z6/btsI1wOtajm/UmyBpY9Ka9TlyC7r33qqP0/img.png&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot; data-is-animation=&quot;false&quot; style=&quot;width: 63.2659%; margin-right: 10px;&quot; data-widthpercent=&quot;64.01&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nb0Z6/btsI1wOtajm/UmyBpY9Ka9TlyC7r33qqP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNb0Z6%2FbtsI1wOtajm%2FUmyBpY9Ka9TlyC7r33qqP0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FZxM8/btsI0IWn8T6/Y8BLZEmqO3LciMuNffh850/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FZxM8/btsI0IWn8T6/Y8BLZEmqO3LciMuNffh850/img.png&quot; data-origin-width=&quot;2249&quot; data-origin-height=&quot;3000&quot; data-is-animation=&quot;false&quot; data-filename=&quot;edited_IMG_5288.jpeg&quot; style=&quot;width: 35.5713%;&quot; data-widthpercent=&quot;35.99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FZxM8/btsI0IWn8T6/Y8BLZEmqO3LciMuNffh850/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFZxM8%2FbtsI0IWn8T6%2FY8BLZEmqO3LciMuNffh850%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2249&quot; height=&quot;3000&quot;/&gt;&lt;/span&gt;&lt;/div&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;요즘의 목표는 멈추지 않고 계속해서 성장하는 사람이 되는 것이다. 어떤 책에선가 본 내용인데 그 사람은 누군가를 평가할 때 현재 가진 능력이나 지식을 보기보다 현재 성장하고 있는가 멈춰있는가를 본다고 한다. 그 말이 되게 감명 깊었다. 취준을 하며 어떻게 해야 매력적인 지원자가 될까에 대해 오래 고민한 적이 있었는데 현재까지의 경험으로 내릴 수 있는 답은 저 말인 것 같다. 올 하반기에도 계속해서 성장해서 연말 회고 때는 지금보다는 더욱 나은 개발자가 되어 있고 싶다.&lt;/p&gt;</description>
      <category>기록/후기, 회고</category>
      <category>2024 상반기 회고</category>
      <category>2024 회고</category>
      <category>싸피</category>
      <category>싸피 회고</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/166</guid>
      <comments>https://gmelon.dev/166#entry166comment</comments>
      <pubDate>Sun, 11 Aug 2024 16:38:25 +0900</pubDate>
    </item>
    <item>
      <title>마지막 싸피인 인터뷰 - 11기 김수영님</title>
      <link>https://gmelon.dev/165</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째이자 싸피 퇴소로 인해 마지막이 될 싸터-뷰(방금 정함) 인터뷰이는 싸피 1학기 마지막 관통 프로젝트를 함께 했던 김수영님이다.&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-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1637592194193.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm6MgA/btsIhxBu9QE/RhvzKJVKkk4XycEJAPmvZk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm6MgA/btsIhxBu9QE/RhvzKJVKkk4XycEJAPmvZk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm6MgA/btsIhxBu9QE/RhvzKJVKkk4XycEJAPmvZk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm6MgA%2FbtsIhxBu9QE%2FRhvzKJVKkk4XycEJAPmvZk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; data-filename=&quot;1637592194193.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 친해지고 스터디와 등산, 싸각런(싸피 모여서 각자 러닝ㅎ), 1학기 마지막 프로젝트까지 함께하며 남들을 잘 배려하고 배움에 대한 열정을 기반으로 뭐든 열심히 하는 모습이 정말 배울 점이 많은 친구라는 생각이 들었다. 자기소개 때 본인은 개발 역량을 갖춘 PM에 관심이 있다고 얘기한 게 기억이 나서 어떤 식으로 커리어를 쌓아가고 싶은지, 어떤 준비를 하고 있는지 궁금한 부분에 대해 중점적으로 물어봤다.&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: #ff6900;&quot;&gt;&lt;b&gt;우선 간단한 자기소개 부탁드립니다 :D&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;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;개발자가&amp;nbsp;되고자&amp;nbsp;결심한&amp;nbsp;계기가&amp;nbsp;무엇인가요?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 가지 이유가 있지만, &lt;b&gt;제가 사용하는 서비스를 직접 만들 수 있다는 점&lt;/b&gt;이 매력적으로 다가왔던 것 같아요. 저는 고등학교 때 문과였는데, 진로에 확신을 가지지 못해서 대학교를 자율전공학과 쪽으로 진학하게 됐어요. 그&amp;nbsp;뒤로&amp;nbsp;1학년&amp;nbsp;때,&amp;nbsp;여러가지&amp;nbsp;전공&amp;nbsp;기초&amp;nbsp;과목들을&amp;nbsp;들어보면서&amp;nbsp;어느&amp;nbsp;쪽을&amp;nbsp;갈지&amp;nbsp;고민해&amp;nbsp;보며,&lt;b&gt;&amp;nbsp;&amp;lsquo;재밌게&amp;nbsp;오래&amp;rsquo;&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;분야가&amp;nbsp;무엇일까?&amp;nbsp;라고&amp;nbsp;고민해 봤고,&lt;/b&gt;&amp;nbsp;위에서&amp;nbsp;얘기한&amp;nbsp;것과&amp;nbsp;같은&amp;nbsp;이유&amp;nbsp;때문에&amp;nbsp;컴퓨터&amp;nbsp;공학과로&amp;nbsp;진로를&amp;nbsp;정하게&amp;nbsp;된&amp;nbsp;것&amp;nbsp;같아요.&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-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1717105746681.jpg&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/47bUD/btsIihkBPIs/tkENDoKuONRhFm2vdlg4v0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/47bUD/btsIihkBPIs/tkENDoKuONRhFm2vdlg4v0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/47bUD/btsIihkBPIs/tkENDoKuONRhFm2vdlg4v0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F47bUD%2FbtsIihkBPIs%2FtkENDoKuONRhFm2vdlg4v0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1664&quot; height=&quot;1111&quot; data-filename=&quot;1717105746681.jpg&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;사실 진로를 결정하고 항상 잘 맞는다고 생각했던 건 아니에요. 최근 몇 년 사이에 개발 분야가 인기가 많아지면서 많은 비전공자분들이 개발자로의 도전을 하시고 있고, 유튜브나 싸피를 봐도 개발 쪽이 잘 맞아서 자신의 역량을 잘 펼치는 분들이 있는데, 저는 제가 결정한 진로에 대해 의심하는 순간들이 너무 많았고, 싸피에 들어와서도 저의 실력이 너무 부족하다고 느껴지는 순간들도 많았어요. 하지만&amp;nbsp;새로운&amp;nbsp;프로젝트를&amp;nbsp;하거나,&amp;nbsp;서비스를&amp;nbsp;만드는&amp;nbsp;시간들이&amp;nbsp;너무&amp;nbsp;재밌고,&amp;nbsp;더&amp;nbsp;좋은&amp;nbsp;서비스에&amp;nbsp;대한&amp;nbsp;욕심은&amp;nbsp;계속&amp;nbsp;생기고&amp;nbsp;있기&amp;nbsp;때문에,&amp;nbsp;거기에서&amp;nbsp;이&amp;nbsp;길을&amp;nbsp;계속&amp;nbsp;갈&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;용기를&amp;nbsp;얻고&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;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;함께 프로젝트를 하며 개발 외에 기획이나 디자인 등 다른 분야의 역량도 뛰어나다는 생각이 들었습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;이후에 관련 분야로의 커리어 전환도 생각이 있으신가요?&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋게&amp;nbsp;평가해&amp;nbsp;주셔서&amp;nbsp;감사합니다&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;제가 스스로 생각하기에는, 제가 가지고 있는 기획이나 디자인 역량이 뭔가 애매한 재능이라고 생각을 하고 있어요. 개발자들보다는 잘 하지만, 전공자에는 전혀 미치지 못하는&amp;hellip; 그래서 기회가 된다면 제대로 배워보고 싶은 마음이 있긴 하지만, 먼저 개발에 대한 역량을 탄탄하게 쌓는 것이 먼저라고 생각하고 있습니다. 커리어&amp;nbsp;전환에&amp;nbsp;대한&amp;nbsp;대답은&amp;nbsp;다음&amp;nbsp;질문의&amp;nbsp;답변&amp;nbsp;내용에&amp;nbsp;함께&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;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;수영님이&amp;nbsp;이루고&amp;nbsp;싶은&amp;nbsp;최종&amp;nbsp;목표는&amp;nbsp;무엇인가요?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 목표는 아직 정하지 못했지만, 커리어를 쌓아가면서 IT 서비스 분야의 PM, PO가 되는 것입니다!! 저는 직접 개발을 하는 것도 좋아하지만, &lt;b&gt;사람들이 사용하는 &amp;lsquo;서비스&amp;rsquo;를 고민하는 과정을 제일 좋아하는 것 같아요&lt;/b&gt;. 그래서&amp;nbsp;저는&amp;nbsp;개발자로서의&amp;nbsp;커리어를&amp;nbsp;쌓아&amp;nbsp;가다가,&amp;nbsp;앞에서&amp;nbsp;말한&amp;nbsp;직무로&amp;nbsp;커리어&amp;nbsp;전환을&amp;nbsp;하고&amp;nbsp;싶다는&amp;nbsp;목표가&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만, IT 분야의 PM이 되기 위해선, 직접 개발을 하는 개발자보다 기술에 대한 이해도가 더 높아야 한다고 생각해요. 때문에&amp;nbsp;적당히&amp;nbsp;1~2년&amp;nbsp;개발자를&amp;nbsp;하다가&amp;nbsp;옮기기보다는,&amp;nbsp;제가&amp;nbsp;선택한&amp;nbsp;분야의&amp;nbsp;개발&amp;nbsp;역량을&amp;nbsp;최대한&amp;nbsp;쌓을&amp;nbsp;수&amp;nbsp;있을&amp;nbsp;만큼&amp;nbsp;오래&amp;nbsp;하고,&amp;nbsp;개발자로서의&amp;nbsp;역량이&amp;nbsp;최대로&amp;nbsp;쌓였다고&amp;nbsp;생각이&amp;nbsp;될&amp;nbsp;때쯤&amp;nbsp;커리어&amp;nbsp;전환을&amp;nbsp;하고&amp;nbsp;싶어요!!&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-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1662032521611.jpg&quot; data-origin-width=&quot;4592&quot; data-origin-height=&quot;3448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9JwH9/btsIjgFiGVS/4d0iwzywsanZEu6wQKYZTK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9JwH9/btsIjgFiGVS/4d0iwzywsanZEu6wQKYZTK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9JwH9/btsIjgFiGVS/4d0iwzywsanZEu6wQKYZTK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9JwH9%2FbtsIjgFiGVS%2F4d0iwzywsanZEu6wQKYZTK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;4592&quot; height=&quot;3448&quot; data-filename=&quot;1662032521611.jpg&quot; data-origin-width=&quot;4592&quot; data-origin-height=&quot;3448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;옆에서&amp;nbsp;지켜보며&amp;nbsp;차분함과&amp;nbsp;세심함이&amp;nbsp;수영님의&amp;nbsp;강점이라는&amp;nbsp;생각이&amp;nbsp;들었습니다.&amp;nbsp;스스로&amp;nbsp;생각하는&amp;nbsp;본인의&amp;nbsp;장점/경쟁력은&amp;nbsp;뭔가요?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각하는 개발자로서의 저의 강점은, &lt;b&gt;&amp;lsquo;열정&amp;rsquo;&lt;/b&gt; 이라고 생각해요. 너무 뻔한 말 같아서 다른 단어를 고민해 봤는데, 결국 저를 제일 잘 표현할 수 있는 말은 열정인 것 같아요. 하지만, 제가 가진 열정은 막연함이 아닌, &lt;b&gt;좋은 프로젝트를 만드는 원동력&lt;/b&gt;이 된다고 생각합니다. 프로젝트에 대한 열정이 있으면, 좋은 서비스를 만들려고 노력하고, 더 좋은 아이디어를 만들려고 노력해요. 또한 세부적인 디테일들도 그냥 넘어가기보다는 신경 써서 더 좋게 만들려고 하게 되구요. 이렇게&amp;nbsp;열정을&amp;nbsp;들여&amp;nbsp;프로젝트를&amp;nbsp;만들면,&amp;nbsp;자연스럽게&amp;nbsp;프로젝트에&amp;nbsp;대한&amp;nbsp;이해도도&amp;nbsp;높아져서&amp;nbsp;발표할&amp;nbsp;때도&amp;nbsp;큰&amp;nbsp;도움이&amp;nbsp;되고&amp;nbsp;있습니다&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;근데&amp;nbsp;사실&amp;nbsp;열정만&amp;nbsp;가지고&amp;nbsp;하면&amp;hellip;&amp;nbsp;디테일에&amp;nbsp;신경 쓰느라&amp;nbsp;정해진&amp;nbsp;시간까지&amp;nbsp;개발을&amp;nbsp;마치지&amp;nbsp;못하는&amp;nbsp;일이&amp;nbsp;생길&amp;nbsp;수도&amp;nbsp;있기&amp;nbsp;때문에&amp;hellip;&amp;nbsp;제가&amp;nbsp;좋은&amp;nbsp;개발자로&amp;nbsp;성장하기&amp;nbsp;위해서는&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;다&amp;nbsp;잡아야&amp;nbsp;할&amp;nbsp;것&amp;nbsp;같습니다&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;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;싸피&amp;nbsp;2학기&amp;nbsp;각오&amp;nbsp;한마디만&amp;nbsp;해주시죠&amp;nbsp;ㅎㅎ&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 일들이 있던 1학기였던 것 같아요.. 제가 목표했던 것에 많이 다가가는 시간이었습니다. 무엇보다&amp;nbsp;좋은&amp;nbsp;사람들을&amp;nbsp;많이&amp;nbsp;만나서&amp;nbsp;보낸&amp;nbsp;시간들이&amp;nbsp;너무&amp;nbsp;소중하고&amp;nbsp;행복했어요&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;하지만, 하고자 했던 것들 중 이루지 못한 것들이 많아서 아쉬움도 많이 남는 학기였어요. 2학기 목표를 아직 구체적으로 세우진 못했지만, 알고리즘 플레 찍기, 3개 프로젝트 중 2개 이상 수상하기, 하반기 채용 때는 면접까지 가보기 등등&amp;hellip; 구체적인 계획을 짜서 1학기보다는 더 열심히 살고, 또한,&amp;nbsp;지멜론님&amp;nbsp;처럼&amp;nbsp;싸피에서&amp;nbsp;만난&amp;nbsp;좋은&amp;nbsp;사람들과&amp;nbsp;계속&amp;nbsp;좋은&amp;nbsp;인연을&amp;nbsp;이어가는&amp;nbsp;것&amp;nbsp;역시&amp;nbsp;2학기를&amp;nbsp;준비하는&amp;nbsp;마음가짐&amp;nbsp;중&amp;nbsp;하나입니다!!&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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6VDLe/btsIjMRu0Om/GUyI95HEJrliIKpyFTRPK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6VDLe/btsIjMRu0Om/GUyI95HEJrliIKpyFTRPK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6VDLe/btsIjMRu0Om/GUyI95HEJrliIKpyFTRPK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6VDLe%2FbtsIjMRu0Om%2FGUyI95HEJrliIKpyFTRPK1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;490&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>기록/SSAFYcial</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/165</guid>
      <comments>https://gmelon.dev/165#entry165comment</comments>
      <pubDate>Mon, 1 Jul 2024 13:19:38 +0900</pubDate>
    </item>
    <item>
      <title>싸피인 인터뷰 2편 - 11기 고승희님</title>
      <link>https://gmelon.dev/163</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지난달 &lt;/span&gt;&lt;a href=&quot;https://sh-hyun.tistory.com/161&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;첫 번째 인터뷰&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에 이어 두 번째 인터뷰를 진행하게 됐다. 이번 인터뷰이는 역시 같은 반의 고승희 님이다. 특히 승희님은 싸피셜로도 함께 활동하고 있고, 평소 기록에 관심이 많은 것 같아 보여서 궁금한 점들이 있었다. 풀스택을 지향하긴 하지만 백엔드를 중점으로 배우게 되는 싸피 과정에 프론트엔드 경험을 가지고 입과 하시기도 해서 요즘 어떤 생각과 고민을 하고 계신지 궁금해 인터뷰를 요청드렸다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;2268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0SpCM/btsF3TGuHgw/vK1wbOpZk3EYiJMBhx3Nu1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0SpCM/btsF3TGuHgw/vK1wbOpZk3EYiJMBhx3Nu1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0SpCM/btsF3TGuHgw/vK1wbOpZk3EYiJMBhx3Nu1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0SpCM%2FbtsF3TGuHgw%2FvK1wbOpZk3EYiJMBhx3Nu1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;2268&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;2268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;평소 개인적으로 어떻게 기록을 하고 있나요?&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;싸피 수업에서 배운 내용은 보통 노션에 기록하는 것 같아요. 환경 설정하는 방법까지도 최대한 적어 놓으려고 노력해요. 왜냐면 나중에 집 가서 혼자 하려고 하면 어렵거든요. 제가 사소한 거 많이 적어놓으니까 친구들이 뭐 기억 안나는 거 있으면 와서 물어보는데 제가 뭐라도 도움을 줄 수 있다는 게 좋은 것 같아요. 최대한 필기해 두기!&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고&amp;nbsp;최근에&amp;nbsp;상혁님&amp;nbsp;개발&amp;nbsp;블로그&amp;nbsp;봤거든요.&amp;nbsp;배운&amp;nbsp;내용을&amp;nbsp;정리하면서&amp;nbsp;본인&amp;nbsp;것으로&amp;nbsp;만드시는&amp;nbsp;것&amp;nbsp;같더라고요.&amp;nbsp;기록하는&amp;nbsp;습관이&amp;nbsp;정말&amp;nbsp;중요한&amp;nbsp;것&amp;nbsp;같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;2560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o6HUD/btsF4Tfbiax/83LN8u68G9K5KkN7LiSqm1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o6HUD/btsF4Tfbiax/83LN8u68G9K5KkN7LiSqm1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o6HUD/btsF4Tfbiax/83LN8u68G9K5KkN7LiSqm1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo6HUD%2FbtsF4Tfbiax%2F83LN8u68G9K5KkN7LiSqm1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;2560&quot; height=&quot;2560&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;2560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;자유 시간에는 어떤 걸로 휴식을 취하거나 스트레스를 푸나요?&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;솔직히&amp;nbsp;유튜브에서&amp;nbsp;코딩과&amp;nbsp;관련 없는&amp;nbsp;영상&amp;nbsp;보면서&amp;nbsp;쉬는&amp;nbsp;것&amp;nbsp;같아요.&amp;nbsp;안&amp;nbsp;풀리는&amp;nbsp;코테&amp;nbsp;문제&amp;nbsp;잊고&amp;nbsp;확&amp;nbsp;쉬려고요.&amp;nbsp;&lt;br&gt;근데 이 부분은 솔직히 좀 반성되는 게, 싸피는 몰입 해서 코딩 공부하는 기간이잖아요. 이때 최대한 많은 시간을 들여서 밀도 있게 공부를 하고 정확히 원리를 이해하고 넘어가야 하는데, 저는 잊고 쉬려고 하는 게 큰 것 같아요. 앞으로는 고등학교 3학년 모드로 시간이 날 때 이해 안 되는 개념을 구글링 하고 찾아보는 게 좋을 것 같아요. 조금 반성.&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고&amp;nbsp;한편으로는&amp;nbsp;'싸각런'이라는&amp;nbsp;런닝&amp;nbsp;크루에서&amp;nbsp;가끔&amp;nbsp;달리기도&amp;nbsp;합니다!&amp;nbsp;싸피에서&amp;nbsp;하루 내내&amp;nbsp;컴퓨터&amp;nbsp;앞에&amp;nbsp;앉아&amp;nbsp;있다 보니&amp;nbsp;점점&amp;nbsp;거북목도&amp;nbsp;심해지고&amp;nbsp;몸도&amp;nbsp;굳는&amp;nbsp;것&amp;nbsp;같아서&amp;nbsp;달리기&amp;nbsp;습관으로&amp;nbsp;체력을&amp;nbsp;조금&amp;nbsp;길러보려고&amp;nbsp;합니다.&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;내가 생각하는 나의 경쟁력은 무엇인가요?&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;백엔드&amp;nbsp;지식을&amp;nbsp;갖춘&amp;nbsp;프론트엔드&amp;nbsp;개발자가&amp;nbsp;되고&amp;nbsp;싶어요.&amp;nbsp;아무래도&amp;nbsp;싸피에서&amp;nbsp;배운&amp;nbsp;내용들이&amp;nbsp;도움이&amp;nbsp;되겠죠.&lt;br&gt;&lt;br&gt;그리고 한편으로는 싸피셜이나 다른 활동을 통해 글쓰기를 꾸준히 하고 있는데 이 부분이 팀원들과 소통하는 데 도움이 될 것 같아요. 또, 글을 쓸 때 사회적인 이슈를 접하는 것이 새로운 프로젝트 아이디어를 내거나 문제의 해결책을 찾는데 필요하다고 생각해요.&lt;br&gt;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;개발자로서 나의 장기적인 커리어 목표가 있나요?&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;당장은 금융업 쪽에서 일해보고 싶어요. 프론트엔드&amp;nbsp;개발을&amp;nbsp;쭉&amp;nbsp;하다가&amp;nbsp;서비스&amp;nbsp;기획으로&amp;nbsp;틀어서,&amp;nbsp;사회에&amp;nbsp;도움이&amp;nbsp;되면서&amp;nbsp;많은&amp;nbsp;사람들이&amp;nbsp;이용하는&amp;nbsp;서비스를&amp;nbsp;만들어&amp;nbsp;보고&amp;nbsp;싶습니다.&lt;br&gt;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;싸피가 나에게 어떤 영향을 주고 있나요?&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;싸피는 저한테 탈출구 같은 거였어요. 대학시절에 전공 공부를 잘 해내지 못했다는 불안감이 컸는데, 싸피에서 안정적인 생활패턴하에 양질의 교육을 받으면서 부족한 지식을 채울 수 있어서 너무 좋은 것 같습니다. 그리고 협업 측면에서도 이전에는 은근히 질문을 못하는 팀원이었는데, 여기서 좋은 동료들을 만나면서 부족함을 인정하고 도움받는 법을 배워가고 있어요. 이렇게 인간관계, 개발 양쪽 측면에서 많이 배우고 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;인간관계가&amp;nbsp;어렵거나&amp;nbsp;취업&amp;nbsp;준비를&amp;nbsp;혼자&amp;nbsp;하기&amp;nbsp;불안하거나&amp;nbsp;개발&amp;nbsp;지식이&amp;nbsp;부족하다면,&amp;nbsp;체계적으로&amp;nbsp;준비하고&amp;nbsp;익힐&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;싸피에&amp;nbsp;꼭&amp;nbsp;한번&amp;nbsp;들어오시길&amp;nbsp;바라요.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/smSS0/btsF3b2kKrd/X9rjVdOivqH0vnRKNIsRCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/smSS0/btsF3b2kKrd/X9rjVdOivqH0vnRKNIsRCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/smSS0/btsF3b2kKrd/X9rjVdOivqH0vnRKNIsRCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsmSS0%2FbtsF3b2kKrd%2FX9rjVdOivqH0vnRKNIsRCK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;490&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;</description>
      <category>기록/SSAFYcial</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/163</guid>
      <comments>https://gmelon.dev/163#entry163comment</comments>
      <pubDate>Tue, 26 Mar 2024 15:05:03 +0900</pubDate>
    </item>
    <item>
      <title>삼성 SW 역량 테스트 B형 취득 후기</title>
      <link>https://gmelon.dev/162</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;며칠 전 3월 9일 싸피 대전캠퍼스에서 응시했던 삼성 SW 역량 테스트 B형 검정에서 합격했다는 메일을 받았다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csOl0E/btsF4T7QnsE/eefkeoCKaqSGr8kMU3Dho0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csOl0E/btsF4T7QnsE/eefkeoCKaqSGr8kMU3Dho0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csOl0E/btsF4T7QnsE/eefkeoCKaqSGr8kMU3Dho0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsOl0E%2FbtsF4T7QnsE%2FeefkeoCKaqSGr8kMU3Dho0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;312&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B형 취득이라는 게 크다면 크고 작다면 작은 성과이겠지만, 개인적으로는 의미가 있는 이벤트여서 기록을 해두고 싶었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;삼성 SW 역량 테스트란&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 삼성 SW 역량 테스트란 삼성 전자에서 수시로 시행하는 코딩테스트로, 개인적으로 응시하기 위해서는 아래의 SW Expert Academy에서 접수할 수 있는 것으로 알고 있다. 나는 싸피를&amp;nbsp;이수하고 있기 때문에 싸피에서 단체로 시험을 응시할 수 있었다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2394&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo2d7Q/btsF2vmdGQK/9iFyqrmlT1bxACJekJIXT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo2d7Q/btsF2vmdGQK/9iFyqrmlT1bxACJekJIXT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo2d7Q/btsF2vmdGQK/9iFyqrmlT1bxACJekJIXT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo2d7Q%2FbtsF2vmdGQK%2F9iFyqrmlT1bxACJekJIXT1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2394&quot; height=&quot;1334&quot; data-origin-width=&quot;2394&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역량 테스트는 A(Advanced), B(Professional), C(Expert) 의 3개 등급으로 구분된다. 전공반의 경우 A/A+형 취득이 2학기로 넘어가기 위한 조건이기에 1학기 수강생들에게 3번의 응시 기회가 주어진다. B형의 경우 A형을 취득한 수강생에 한하여 학기에 3~4번 정도 응시 기회가 주어진다. 한 기수가 천명 정도되는데 그중 60-80명 정도가 B형을 취득하고 수료한다고 알고 있다. (싸피에서는 B형을 취득하면 100만 원의 축하금(?)도 준다.)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;B형 문제 유형&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 문제를 꼼꼼하게 읽고 오류 없이 그대로 코드로 구현해 낼 수 있는지를 묻는 A형과 달리, B형에서는 구현 + 최적화를 요구하는 문제들이 주로 출제된다. 간혹 세그먼트 트리, KMP 등등 알고리즘 자체가 어려운 유형도 출제되는 것 같지만, 특히 최근에는 문제를 주어진 그대로 구현하면 시간 초과가 발생하여 간단한 알고리즘, 자료구조를 적절히 활용해서 시간 복잡도를 개선해야 하는 문제가 출제되는 것 같다. 4시간에 1문제를 풀기 때문에 최초 1-2시간은 설계에 투자하고 1시간 구현, 1시간 디버깅과 같이 &lt;span style=&quot;color: #333333;&quot;&gt;시간을 잘 분배해서&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 문제를 푸는 게 중요하다고 느꼈다.&lt;/span&gt;&lt;br&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;나는 알고리즘을 정말 싫어했다. 학부 때 어떤 교수님의 알고리즘 수업을 듣게 되었는데, 매 수업시간이 끝날 때마다 자체 코테 플랫폼에 문제를 오픈하고 2시간 내에 풀지 못하면 x점 감점, 당일까지 풀지 못하면 또 y점 감점, 다음 주 수업 전까지 풀지 못하면 z점 감점과 같은 방식으로 실습과 평가가 진행되는 수업이었다. 그전까지 알고리즘이란 걸 제대로 공부해 본 적 없던 나는 계속 당일에 코드를 제출하지 못했다. 결국 새벽에 두 시간 정도 산책하고 집에 들어와서 그 수업을 철회했던 기억이 있다. (그리고 그 다음 해에 다른 교수님 수업을 들었다ㅜ)&lt;br&gt;&amp;nbsp;&lt;br&gt;이때 나는 '아 나는 알고리즘이란 걸 평생 잘할 수 없겠구나'라고 생각했다. 지금 생각해 보면 제대로 공부해보지도 않고 무서워서 도망쳤던 건데 막연하게 나랑은 맞지 않는 분야라고 생각했던 것 같다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;스터디 &amp;amp; 싸피&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 학부를 마치고, 취업을 위해 학과 사람들과 미루고 미루던 알고리즘 스터디를 하게 됐다. 다들 알고리즘을 제대로 공부해 본 적이 오래됐거나 처음이라서 아래의 나동빈님 파이썬 코딩테스트 책을 구입해서 커리큘럼을 쭉 따라 진행했다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKNQm7/btsF4xw5mH3/eJcuGWkKHB8LOLlEYX29E1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKNQm7/btsF4xw5mH3/eJcuGWkKHB8LOLlEYX29E1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKNQm7/btsF4xw5mH3/eJcuGWkKHB8LOLlEYX29E1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKNQm7%2FbtsF4xw5mH3%2FeJcuGWkKHB8LOLlEYX29E1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1248&quot; height=&quot;552&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 단편적/수동적으로 공부할 때와 달리, 스터디라는 체계와 책에서 제공하는 커리큘럼을 통해 보다 더 정교하게 알고리즘을 학습할 수 있었다. 이때 스택, 큐, 힙과 같이 기본적인 자료구조나 그리디, DP, 그래프 등 코테에 자주 출제되는 알고리즘을 익히게 되었다. 한 6개월 정도 공부하니 그래도 백준 골드 쉬운 문제들을 어떻게 풀 수 있는 실력이 되었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그러고 나서 올해 싸피에 입과해&amp;nbsp;한 달간 싸피의 알고리즘 커리큘럼을 이수했다. 아는 내용도 있고 모르는 내용도 있었지만, 검증된 커리큘럼을 통해 학습하니 머릿속에서 내용들이 체계적으로 정리되는 것을 느꼈다. 가장 좋았던 건 아무래도 혼자 공부할 때는 문제 풀이에 시간을 많이 투자하기가 쉽지 않은데 싸피에서는 9-6로 다 같이 주어진 문제들을 풀게 된다는 점이다. 이 과정에서 구현 역량이 엄청 느는 것을 느꼈다. 그 결과 2학기를 위한 A형과 B형을 차례로 취득할 수 있었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;나에게 B형 취득이 의미 있었던 이유는, 이건 절대로 못할 거라고 생각했던 것을 적절한 방향과 방법으로 노력한 결과 이룰 수 있었기 때문이다. 이러한 성취가 단순히 알고리즘뿐 아니라, 다른 분야에도 적용 가능할 것이라고 생각했고 이런 식의 크고 작은 도전과 성취 경험을 쌓는 것이 계속해서 성장해 나가는 데에 도움을 줄 것이라고 생각했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;앞으로도 분명 많은 어려움과 좌절이 있겠지만, 과거의 경험들을 떠올리며 힘들더라도 꾸준히 좋은 방향으로 노력하는 개발자가 되어야겠다고 다짐했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nX3jD/btsF3nIjhsY/PeLZj3OVGRO38buUao6LW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nX3jD/btsF3nIjhsY/PeLZj3OVGRO38buUao6LW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nX3jD/btsF3nIjhsY/PeLZj3OVGRO38buUao6LW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnX3jD%2FbtsF3nIjhsY%2FPeLZj3OVGRO38buUao6LW0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;490&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;</description>
      <category>기록/SSAFYcial</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/162</guid>
      <comments>https://gmelon.dev/162#entry162comment</comments>
      <pubDate>Sun, 24 Mar 2024 23:39:05 +0900</pubDate>
    </item>
    <item>
      <title>싸피인 인터뷰 1편 - 11기 김민지님</title>
      <link>https://gmelon.dev/161</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;싸피의 기자단인 싸피셜은 월에 자율 기사와 기획 기사를 각 1편씩 작성하게 된다. 1학기 기획 기사 주제를 뭘로 할지 고민하다가 뛰어난 주변 동기분들을 보며 저분들은 어떤 노력을 해왔고 어떤 강점을 가지고 있을까 궁금해져 한 달에 한 분씩 인터뷰를 진행해보기로 했다. 그리하야 첫 번째 인터뷰이는 11기 대전 6반 &lt;/span&gt;&lt;a href=&quot;https://github.com/meanz1&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;김민지&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #000000;&quot;&gt;님이다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중앙 통로를 사이에 두긴 하지만 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;첫 달에 민지님과 같은 행에 앉게 되었는데 강사님과 주변 사람들에게 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;누구보다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;열심히&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 물어보고 고민하며 열정적으로 학습하는 것을 보면서 굉장히 적극적이고 열정적인 사람이구나 하는 생각을 했었다. github 팔로우를 하게되면서 구경한 프로필 README에 적힌 '열정적인 시간을 좋아합니다' 라는 소개 문구를 보며 적극성이 자신의 장점임을 잘 알고 활용하고 있구나 라는 생각도 했다. 나는 남이 시키는 걸 열심히&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하는 건&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잘하는데 적극적으로 찾아서 학습하고&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하는 건&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잘 못해서 되게 부러운 강점이라고 생각했고,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떻게 하면 저런 학습 태도를 가질 수 있을지 궁금해서 인터뷰를 요청드렸다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;먼저 간단한 자기소개 부탁드립니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CN7XL/btsFg4aT8pT/JzDoXVUgQ4KozP43ifJgZ1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CN7XL/btsFg4aT8pT/JzDoXVUgQ4KozP43ifJgZ1/img.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot; style=&quot;width: 56.4784%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CN7XL/btsFg4aT8pT/JzDoXVUgQ4KozP43ifJgZ1/img.jpg&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCN7XL%2FbtsFg4aT8pT%2FJzDoXVUgQ4KozP43ifJgZ1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y4O4e/btsFgQw19cG/9ZRMdxyQKjySsQ3zg3wpn0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y4O4e/btsFgQw19cG/9ZRMdxyQKjySsQ3zg3wpn0/img.jpg&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;1440&quot; style=&quot;width: 42.3588%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y4O4e/btsFgQw19cG/9ZRMdxyQKjySsQ3zg3wpn0/img.jpg&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY4O4e%2FbtsFgQw19cG%2F9ZRMdxyQKjySsQ3zg3wpn0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;810&quot; height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요~ 싸피 11기 대전캠퍼스 6반 김민지입니다! 저는 ENFP로 밝고 자유로운 사람입니다. 건강과 행복을 삶의 목표이자 기준으로 삼고 있습니다. 또 빵을 굉장히 좋아하는데요, 그래서 빵의 도시 대전에서 싸피 생활을 아주아주 즐겁게 하는 중입니다. (성심당 말고도 맛있는 빵집이 정~~말 많더라고요! ‘파이룸’ 강추합니다!!)&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;iframe id=&quot;maps_1708853874909&quot; data-ke-type=&quot;map&quot; mapdata=&quot;addr=%EB%8C%80%EC%A0%84%20%EC%9C%A0%EC%84%B1%EA%B5%AC%20%EB%B4%89%EB%AA%85%EB%8F%99%20607-4%201%EC%B8%B5%20103%ED%98%B8&amp;amp;addtype=1&amp;amp;confirmid=307496139&amp;amp;docid=&amp;amp;idx=1&amp;amp;ifrH=329px&amp;amp;ifrW=550px&amp;amp;mapHeight=329&amp;amp;mapInfo=%7B%22version%22%3A2%2C%22mapWidth%22%3A550%2C%22mapHeight%22%3A329%2C%22mapCenterX%22%3A576860%2C%22mapCenterY%22%3A794450%2C%22mapLevel%22%3A4%2C%22coordinate%22%3A%22wcongnamul%22%2C%22markInfo%22%3A%5B%7B%22markerType%22%3A%22standPlace%22%2C%22coordinate%22%3A%22wcongnamul%22%2C%22x%22%3A576863%2C%22y%22%3A794450%2C%22clickable%22%3Atrue%2C%22draggable%22%3Atrue%2C%22icon%22%3A%7B%22width%22%3A35%2C%22height%22%3A56%2C%22offsetX%22%3A17%2C%22offsetY%22%3A56%2C%22src%22%3A%22%2F%2Ft1.daumcdn.net%2Flocalimg%2Flocalimages%2F07%2F2012%2Fattach%2Fpc_img%2Fico_marker2_150331.png%22%7D%2C%22content%22%3A%22%ED%8C%8C%EC%9D%B4%EB%A3%B8%22%2C%22confirmid%22%3A307496139%7D%5D%2C%22graphicInfo%22%3A%5B%5D%2C%22roadviewInfo%22%3A%5B%5D%7D&amp;amp;mapWidth=550&amp;amp;mapX=576860&amp;amp;mapY=794450&amp;amp;map_hybrid=false&amp;amp;map_level=4&amp;amp;map_type=TYPE_MAP&amp;amp;rcode=3020053000&amp;amp;tel=042-361-5000&amp;amp;title=%ED%8C%8C%EC%9D%B4%EB%A3%B8&quot; data-maps-data=&quot;addr=%EB%8C%80%EC%A0%84%20%EC%9C%A0%EC%84%B1%EA%B5%AC%20%EB%B4%89%EB%AA%85%EB%8F%99%20607-4%201%EC%B8%B5%20103%ED%98%B8&amp;amp;addtype=1&amp;amp;confirmid=307496139&amp;amp;docid=&amp;amp;idx=1&amp;amp;ifrH=329px&amp;amp;ifrW=550px&amp;amp;mapHeight=329&amp;amp;mapInfo=%7B%22version%22%3A2%2C%22mapWidth%22%3A550%2C%22mapHeight%22%3A329%2C%22mapCenterX%22%3A576860%2C%22mapCenterY%22%3A794450%2C%22mapLevel%22%3A4%2C%22coordinate%22%3A%22wcongnamul%22%2C%22markInfo%22%3A%5B%7B%22markerType%22%3A%22standPlace%22%2C%22coordinate%22%3A%22wcongnamul%22%2C%22x%22%3A576863%2C%22y%22%3A794450%2C%22clickable%22%3Atrue%2C%22draggable%22%3Atrue%2C%22icon%22%3A%7B%22width%22%3A35%2C%22height%22%3A56%2C%22offsetX%22%3A17%2C%22offsetY%22%3A56%2C%22src%22%3A%22%2F%2Ft1.daumcdn.net%2Flocalimg%2Flocalimages%2F07%2F2012%2Fattach%2Fpc_img%2Fico_marker2_150331.png%22%7D%2C%22content%22%3A%22%ED%8C%8C%EC%9D%B4%EB%A3%B8%22%2C%22confirmid%22%3A307496139%7D%5D%2C%22graphicInfo%22%3A%5B%5D%2C%22roadviewInfo%22%3A%5B%5D%7D&amp;amp;mapWidth=550&amp;amp;mapX=576860&amp;amp;mapY=794450&amp;amp;map_hybrid=false&amp;amp;map_level=4&amp;amp;map_type=TYPE_MAP&amp;amp;rcode=3020053000&amp;amp;tel=042-361-5000&amp;amp;title=%ED%8C%8C%EC%9D%B4%EB%A3%B8&quot; data-maps-mapx=&quot;&quot; data-maps-mapy=&quot;&quot; data-maps-thumbnail=&quot;https://ssl.daumcdn.net/map3/staticmap/image?center=576860%2C794450&amp;amp;lv=4&amp;amp;size=540x350&amp;amp;srs=WCONGNAMUL&amp;amp;markers=symbol%3Asc_marker%7Clocation%3A576863%2C794450&quot; width=&quot;540px&quot; height=&quot;350px&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; src=&quot;/proxy/plusmapViewer.php?id=maps_1708853874909&quot;&gt;&lt;/iframe&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;민지님이 개발자라는 진로를 택하게 된 계기가 뭔지 궁금해요.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8WVjo/btsFgqk9FYW/3JHKr8M0PrpM1gxyiLEzRk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8WVjo/btsFgqk9FYW/3JHKr8M0PrpM1gxyiLEzRk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8WVjo/btsFgqk9FYW/3JHKr8M0PrpM1gxyiLEzRk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8WVjo%2FbtsFgqk9FYW%2F3JHKr8M0PrpM1gxyiLEzRk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;782&quot; height=&quot;1044&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 개발자로 진로를 결정한 지 얼마 되지 않았어요. 대부분의 경험을 다 재미있다고 느끼는 편이라 진짜 내가 즐기면서 꾸준하게 할 수 있는 직업이 무엇인지 오랫동안 고민을 했던 것 같아요. 저는 제 손으로 무언가 만들어 내고 결과에 반영이 되는 과정에서 남들보다 특히 더 큰 기쁨과 성취감을 느끼는 사람인 것 같아요. 그래서 그 도파민을 계속적으로 느끼고자 개발자로 진로를 선택하게 되었습니다!&lt;br&gt;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;민지님을 보면서 참 열정 넘치고 적극적인 사람이다라는 생각을 많이 했습니다. 본인이 생각하는 적극성의 근원이 뭔지, 그리고 이러한 장점을 어떻게 활용하고 계시는지 궁금합니다.&lt;/b&gt;&lt;/span&gt;&lt;br&gt;제 적극성의 근원은 저 자신에 대한 욕심인 것 같습니다. 남들과 비교하기보다는 절대적인 기준으로 스스로가 잘하고 싶은 마음이 큰 것 같아요. 그래서 제가 부족한 부분이 부끄럽지 않기에, 강사님과 친구들에게 질문하며 부족한 제 모습을 채우고 있습니다.&lt;br&gt;저는 제가 해야 하는 것들을 최대한 열심히 하려고 하는 편이에요. 이왕 해야 할 거 만족할만한 결과가 나오면 뿌듯하고 기쁘니까요~ 또 그런 열심히 하는 제 모습을 스스로 좋아합니다 ㅎㅎ&lt;br&gt;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;그밖에 자신의 강점이 있다면 소개해주세요.&lt;/b&gt;&lt;/span&gt;&lt;br&gt;저는 엄청 긍정적입니다. ‘하면 된다. 안 되는 건 없다.’ 라는 마인드를 기본으로 장착하고 있는 것 같아요. 그래서 어려움이 있어도 큰 불만 없이 묵묵하게 합니다. 어차피 잘 될 거니까요~! 또 취준 중인 요즘에는 ‘오히려 좋아. 자소서 소재다.’ 라고 생각하며 어려움을 기쁜 마음으로 맞이하고 있습니다.&lt;br&gt;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;개발자로서 이루고 싶은 (단기 또는 중장기) 목표가 있다면?&lt;/b&gt;&lt;/span&gt;&lt;br&gt;저는 '누구나 함께 일하고 싶은 개발자'가 되고 싶어요! 제가 생각하는 함께 일하고 싶은 사람은 ‘이 사람이랑 일하면 배울 점이 많고, 일하는 과정이 즐겁다!' 라는 느낌을 줄 수 있는 사람인데요~ 그래서 저 또한 열심히 학습하고 다양한 경험을 쌓아 최종적으로는 뛰어난 실력과 사람에 대해 따뜻함을 가지고 있는 시니어 개발자로 성장하고 싶습니다.&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #ff6900;&quot;&gt;&lt;b&gt;싸피에 오기 전에 어떤 준비를 해왔고, 싸피를 통해 얻어가고 싶은 것은&amp;nbsp;무엇인가요?&lt;/b&gt;&lt;/span&gt;&lt;br&gt;대학교를 다니면서 학부 연구생, 인턴, 어학연수 등 다양한 경험을 해봤던 것 같아요. 그치만 이제 개발자로서 깊이 있게 몰두해서 진행해 본 프로젝트가 없는 것 같아 그런 아쉬움을 달래고자 싸피에 오게 되었습니다! 1학기 알고리즘을 마스터하고, 얼른 2학기에 열정적인 친구들과 정말 치열하게 프로젝트 해보고 싶네요. 화이팅 !!&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2gqjJ/btsFiOFeIMn/LpIcT2i2cQRUPXjrkQ6stK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2gqjJ/btsFiOFeIMn/LpIcT2i2cQRUPXjrkQ6stK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2gqjJ/btsFiOFeIMn/LpIcT2i2cQRUPXjrkQ6stK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2gqjJ%2FbtsFiOFeIMn%2FLpIcT2i2cQRUPXjrkQ6stK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;490&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;</description>
      <category>기록/SSAFYcial</category>
      <category>SSAFYcial</category>
      <category>싸피셜</category>
      <category>인터뷰</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/161</guid>
      <comments>https://gmelon.dev/161#entry161comment</comments>
      <pubDate>Wed, 28 Feb 2024 12:49:23 +0900</pubDate>
    </item>
    <item>
      <title>AWS 프리티어 종료 그 후..</title>
      <link>https://gmelon.dev/160</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;작년 말에 배포했던 &lt;a href=&quot;https://sh-hyun.tistory.com/151&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;프로젝트 '플랭고'&lt;/span&gt;&lt;/a&gt; 의 AWS 프리티어가 2024년 1월을 마지막으로 종료되었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;AWS 프리티어란 아래와 같이 EC2, S3, RDS 등등 AWS의 주요 서비스들을 최초 가입 후 1년간 (일정 사양/용량에 한하여) 무료로 사용할 수 있게 제공해 주는 것을 말한다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2450&quot; data-origin-height=&quot;2154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhEvHP/btsEYzW8UnW/WPzvJ2RSJkDKx5E3MZ8D00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhEvHP/btsEYzW8UnW/WPzvJ2RSJkDKx5E3MZ8D00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhEvHP/btsEYzW8UnW/WPzvJ2RSJkDKx5E3MZ8D00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhEvHP%2FbtsEYzW8UnW%2FWPzvJ2RSJkDKx5E3MZ8D00%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2450&quot; height=&quot;2154&quot; data-origin-width=&quot;2450&quot; data-origin-height=&quot;2154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 친절한 AWS는 이미 몇달 전부터 나에게 메일로 &lt;b&gt;너 프리티어 곧 끝나니깐 조심해&lt;/b&gt; 라고 알려주었지만&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kYBs2/btsE1FuYv2Q/FHUDNqvuTwxgkvdw7ZFjX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kYBs2/btsE1FuYv2Q/FHUDNqvuTwxgkvdw7ZFjX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kYBs2/btsE1FuYv2Q/FHUDNqvuTwxgkvdw7ZFjX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkYBs2%2FbtsE1FuYv2Q%2FFHUDNqvuTwxgkvdw7ZFjX0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1766&quot; height=&quot;1846&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1846&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;1월에 싸피가 시작되고 정신이 없던 와중이라 그냥 한 달만 냅둬보고 감당 안 되면 이사 가지 뭐~ 라고 생각 &lt;s&gt;합리화&lt;/s&gt; 를 하게 되었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇게 2월이 시작되고 평화롭게 싸피 생활을 하고 있던 나에게 한 통의 메일이 날아오는데&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2302&quot; data-origin-height=&quot;948&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmlJOi/btsEZoAIe50/Kylle0TdZ6mY06dOxhr5L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmlJOi/btsEZoAIe50/Kylle0TdZ6mY06dOxhr5L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmlJOi/btsEZoAIe50/Kylle0TdZ6mY06dOxhr5L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmlJOi%2FbtsEZoAIe50%2FKylle0TdZ6mY06dOxhr5L1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2302&quot; height=&quot;948&quot; data-origin-width=&quot;2302&quot; data-origin-height=&quot;948&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;?!&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;1162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zY9Z9/btsEYLQNnW3/eUwQQ6n81MwxOM8nvTCyX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zY9Z9/btsEYLQNnW3/eUwQQ6n81MwxOM8nvTCyX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zY9Z9/btsEYLQNnW3/eUwQQ6n81MwxOM8nvTCyX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzY9Z9%2FbtsEYLQNnW3%2FeUwQQ6n81MwxOM8nvTCyX1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1438&quot; height=&quot;1162&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;1162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네?!&lt;br&gt;&amp;nbsp;&lt;br&gt;한 달에 한 만 원 내외로 나오면 그냥 사용할까 싶었는데 세상에 일주일 만에 34,000원이 나와버렸다. 그냥 냅두면 한 달에 10만원이 넘게 나올뻔한 셈이다. 어디서 이렇게 많이 나왔는지 살펴보니&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2142&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chfb7e/btsE0HTWwVV/gTFAOHTiJbGIXRR2uRp7zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chfb7e/btsE0HTWwVV/gTFAOHTiJbGIXRR2uRp7zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chfb7e/btsE0HTWwVV/gTFAOHTiJbGIXRR2uRp7zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchfb7e%2FbtsE0HTWwVV%2FgTFAOHTiJbGIXRR2uRp7zk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2142&quot; height=&quot;898&quot; data-origin-width=&quot;2142&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;흠 골고루 많이 나오고 있었다. (24년 2월 시행된 AWS의 public IP 요금 정책 변경과 맞물려 더 많이 나오기도 했다ㅜㅜ)&lt;br&gt;&amp;nbsp;&lt;br&gt;다행히도(?) 이걸 발견한 당일이 설 연휴의 첫날이었고 바로 짐 싸서 부랴부랴 이사 준비를 하기 시작했다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bykuWW/btsE4Z7BJ0m/5uQzWrM0MmOsguNEIXukj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bykuWW/btsE4Z7BJ0m/5uQzWrM0MmOsguNEIXukj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bykuWW/btsE4Z7BJ0m/5uQzWrM0MmOsguNEIXukj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbykuWW%2FbtsE4Z7BJ0m%2F5uQzWrM0MmOsguNEIXukj0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;360&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;??? : 태초마을이야!&lt;/s&gt; 이 짤이 작업하는 내내 떠올라서 웃기고 슬펐다.&lt;br&gt;&amp;nbsp;&lt;br&gt;S3에 업로드된 이미지나 RDS 데이터는 각각 aws cli와 mysqldump 등으로 쉽게 이전할 수 있었지만 EC2에 docker나 nginx 등 자잘하게 설정된 항목들이랑 Load Balancing CodeDeploy 등 배포 관련 설정들에서 꽤나 시간을 잡아먹었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그래도 결국 이틀 동안 열심히 삽질해서 이사에 성공했고 도메인을 따로 구입해서 사용하고 있었던 터라 앱 배포를 새로 하지 않고 서비스도 계속 유지할 수 있었다. 앞으로 주변에 프리티어가 종료될 예정인 분들을 만나면 꼭 미리미리 이사 가시라고 얘기해 줘야겠다고 생각했다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K5FI7/btsE1EbLaPn/2Ivc2drAHWsMLUd9hmaR0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K5FI7/btsE1EbLaPn/2Ivc2drAHWsMLUd9hmaR0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K5FI7/btsE1EbLaPn/2Ivc2drAHWsMLUd9hmaR0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK5FI7%2FbtsE1EbLaPn%2F2Ivc2drAHWsMLUd9hmaR0K%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;490&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;</description>
      <category>기록/SSAFYcial</category>
      <category>AWS 프리티어 종료</category>
      <category>태초마을이야</category>
      <category>프리티어 종료</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/160</guid>
      <comments>https://gmelon.dev/160#entry160comment</comments>
      <pubDate>Sun, 18 Feb 2024 21:42:48 +0900</pubDate>
    </item>
    <item>
      <title>자바 Collector 구조 (feat. groupingBy() 사용법)</title>
      <link>https://gmelon.dev/159</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collectors의 groupingBy() 메서드를 사용하면 아래와 같이 데이터 스트림을 맵으로 편리하게 반환할 수 있어서 프로젝트나 알고리즘 문제를 풀 때 종종 사용했었다. 그런데 쓸 때마다 groupingBy() 메서드의 API 문서를 보며 아 이렇게 쓰는 거였지 하고 그대로 따라 치곤 했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class GroupingByTest {
    @Getter
    static class Person {

        private final int age;
        private final String name;

        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }
    }

    public static void main(String[] args) {
        List&amp;lt;Person&amp;gt; people = List.of(
            new Person(10, &quot;나&quot;),
            new Person(20, &quot;너&quot;),
            new Person(10, &quot;얘&quot;),
            new Person(10, &quot;쟤&quot;)
        );

        Map&amp;lt;Integer, List&amp;lt;String&amp;gt;&amp;gt; map = people.stream()
            .collect(Collectors.groupingBy(Person::getAge, mapping(Person::getName, toList())));

        System.out.println(map); // {20=[너], 10=[나, 얘, 쟤]}
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 한 번은 평소와 조금 다른 방식으로 이 메서드를 써야할 일이 생겼고, API 문서의 예시만 그대로 따라 치던 나는 처량하게 ChatGPT에게 달려갈 수밖에 없었다!&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;그래서 평소에 궁금했던 Collector의 내부 구조와 동작 방식에 대해 간단하게나마 공부해보았고, 그 내용을 정리했다.&lt;/p&gt;
&lt;h2 id=&quot;collectorsgroupingby&quot; data-ke-size=&quot;size26&quot;&gt;Collectors.groupingBy()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 groupingBy()의 간단한 사용법과 시그니처에 대해 살펴보았다. 메서드는 3개 정도로 오버로딩되어있지만 제일 많이 썼던 메서드(위 코드에서도 사용했던)를 가져와보면 아래와 같다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/vIicQ/btsDqlsiCcs/ZRNQDSsh4NkKW2oyJtOGgk/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 문서는 아래와 같은데,&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/xvqfV/btsDs2Fb7Uj/DlGsaQS5y7Ke8bweARU2xk/img.png&quot; alt=&quot;Untitled&quot; /&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&gt;이 메서드는 T 타입 입력에 대해 &amp;lsquo;group by&amp;rsquo; 작업을 수행하는 Collector 의 구현체를 반환한다&lt;/li&gt;
&lt;li&gt;Function 타입의 classifier는 T 타입 입력을 K 타입의 key로 매핑한다&lt;/li&gt;
&lt;li&gt;Collector 타입의 downstream은 T 타입 입력을 받아 D 타입의 결과(value) 를 반환한다&lt;/li&gt;
&lt;li&gt;결과적으로 반환되는 Collector는 Map&amp;lt;K, D&amp;gt; 을 생성한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 친구를 이해하기 위해서는 이 메서드가 반환한다는 Collector 인터페이스에 대해 더 정확히 이해할 필요가 있어 보였다.&lt;/p&gt;
&lt;h2 id=&quot;collector&quot; data-ke-size=&quot;size26&quot;&gt;Collector&lt;/h2&gt;
&lt;h3 id=&quot;개요&quot; data-ke-size=&quot;size23&quot;&gt;개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collector의 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;API 문서&lt;/a&gt;는 조금 긴데, 나름대로 번역 &amp;amp; 요약해 보면 아래와 같다.&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;&lt;b&gt;Collector는 가변 결과 container에 입력 원소들을 누적하는 가변 reduction operation을 정의한다&lt;/b&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;reduction operation은 순차적으로 또는 병렬로 실행될 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가변 reduction operation의 예시로는 원소들을 Collection으로 누적하거나 문자열을 StringBuilder를 사용해 concat 하거나 원소들의 sum, min, max 등을 구하는 연산들이 있다&lt;/li&gt;
&lt;li&gt;Collector는 원소를 가변 결과 container에 누적하기 위해 사용되는 (또한, 선택적으로 결과를 변형해주는) 아래의 &lt;b&gt;4가지 추상 메서드를 통해 정의&lt;/b&gt;된다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Supplier&lt;a&gt; supplier() - 새로운 결과 container를 생성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;BiConsumer&amp;lt;A, T&amp;gt; accumulator() - 새로운 입력 원소를 기존 결과 container에 결합&lt;/li&gt;
&lt;li&gt;BinaryOperator&lt;a&gt; combiner() - 두 개의 결과 container를 병합&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Function&amp;lt;A, R&amp;gt; finisher() - 결과 container에 대해 마지막 (선택적인) 변형 연산을 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;내부에 Characteristics 라는 enum을 정의하여 연산의 특성을 명시할 수 있다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CONCURRENT - 이 Collector의 누적 연산이 서로 다른 쓰레드에서 병렬로 처리될 수 있음&lt;/li&gt;
&lt;li&gt;UNORDERED - 이 Collector의 연산이 입력 순서를 유지함을 보장하지 않음&lt;/li&gt;
&lt;li&gt;IDENTITY_FINISH - finisher() 에서 변형 연산을 수행하지 않음. 따라서 결과 container의 타입 A 에서 최종 타입 R 로 unchecked cast를 수행해도 오류가 발생하지 않음을 보장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;static 팩토리 메서드 of(Supplier, BiConsumer, BinaryOperator, Characteristics&amp;hellip;) 를 통해 직접 Collector의 구현체를 만들어 사용할 수도 있다&lt;/li&gt;
&lt;li&gt;Collectors 클래스에서 Collector 를 구현하는 여러 편의메서드를 제공한다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;groupingBy(), toList(), toSet(), joining() 등이 여기에 해당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;reduction-operation&quot; data-ke-size=&quot;size23&quot;&gt;reduction operation?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 문서 내내 등장하는 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Reduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;reduction operation&lt;/a&gt;은 동일한 combining 연산을 수행하며 입력 스트림을 하나의 결과로 만드는 연산으로, &lt;b&gt;fold&lt;/b&gt; 라고도 불린다. 예를 들면 아래와 같은 연산이 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;int sum = numbers.stream()
            .reduce(0, (x, y) -&amp;gt; x + y);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 풀어쓰면 아래 코드와 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;int sum = 0;
for (int x : numbers) {
   sum += x;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는 학교에서 Ocaml로 함수형 프로그래밍을 처음 배울 때 fold라는 개념을 처음 접했었는데 자바에서 다시 만나 반가웠다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/nr20z/btsDp8UgBH3/I3ytrmrhlwWM88tC9uS8fK/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;a href=&quot;https://cs3110.github.io/textbook/chapters/hop/fold.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cs3110.github.io/textbook/chapters/hop/fold.html&lt;/a&gt;)&lt;/p&gt;
&lt;h3 id=&quot;타입-파라미터&quot; data-ke-size=&quot;size23&quot;&gt;타입 파라미터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 문서에도 적혀있지만 Collector는 T, A, R 이라는 3개의 타입 파라미터를 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;T - reduction 연산의 대상이 되는 입력 원소&lt;/li&gt;
&lt;li&gt;A - 누적 결과 컨테이너의 타입&lt;/li&gt;
&lt;li&gt;R - 최종 결과 타입 (Characteristics.IDENTITY_FINISH 인 경우 A == R)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;전체-동작-과정&quot; data-ke-size=&quot;size23&quot;&gt;전체 동작 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 이해한 바로는 Collector는 단순히 &lt;b&gt;연산 과정을 정의하는 도구&lt;/b&gt;이고 Collector의 API를 적절히 호출하여 누적 연산이 수행된 결과를 얻을 수 있는 것 같다. API 문서의 가장 아래에 보면 Collector를 어떻게 사용해야 하는지가 명시되어 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cI2qic/btsDuycO1Rr/aPaQhN3O09BtbVFI7F9sJK/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서를 참고해서 전체 과정을 도식화해 보면 아래와 같다.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/COcED/btsDqkz6UDi/MRLMkxRYegYCBL9OpH1Jrk/img.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 만약 accumulate 작업이 병렬로 수행될 경우, combiner() 메서드를 사용해 각각의 결과 container를 container로 병합하는 과정이 추가될 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;streamcollectcollector&quot; data-ke-size=&quot;size23&quot;&gt;Stream.collect(Collector)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream에서는 데이터의 연산을 종결하기 위해 collect() 메서드를 사용할 수 있는데 이 메서드에서 위 그림의 과정을 진행하여 데이터에 연산을 수행하고 결과를 반환해 준다. Stream.collect()의 구현체는 ReferencePipeline 추상 클래스이며 아래와 같이 코드가 구현되어 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/lSh0l/btsDrmRKDIh/eEYyGVdVqeAO10KoCa5OKK/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;h2 id=&quot;collectors&quot; data-ke-size=&quot;size26&quot;&gt;Collectors&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 Collector의 API 문서에 적혀있던 것처럼 Collectors는 자주 사용되는 활용들에 대해 &lt;b&gt;Collector의 구현체를 생성&lt;/b&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;대표적인 예시로 toList()가 있다. 아마 스트림에서 아래와 같은 형태를 가장 많이 사용하지 않을까 싶은데&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;list.stream()
  .map(...)
  .sorted(...)
  ...
    .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 마지막에 사용되는 toList()가 바로 Collectors의 편의 메서드이다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pwkax/btsDuxkHESz/m3av429CCtIahF7VRPIEz0/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Colletor의 구조를 이해하고 나서 다시 메서드를 살펴보니 이제야 어떤 식으로 Collector가 구성되고 사용되는지가 보였다. 먼저 toList()는 원소들을 리스트에 담아야 하므로 결과 container로 사용할 ArrayList를 생성하는 메서드 레퍼런스를 supplier()로 전달하여 사용하고 있는 것을 알 수 있다. 그리고 List.add() 메서드를 accumulator() 로 사용하여 앞서 생성한 ArrayList에 원소를 하나씩 담는다. 그리고 combiner()로 두 ArrayList를 병합해 주는 람다식을 넘겨준다.&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;그리고 마지막으로 CH_ID라는 상수를 넘겨준다. 위에서 사용된 CollectorImpl의 생성자는 아래와 같은데,&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/NHaHr/btsDsXjC87n/LXHGAHMKEmZ9U8yBMGwKt0/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 생성자는 finisher에서 별다른 작업을 하지 않는 경우 사용하는 생성자로 보인다. 실제로 CH_ID 상수를 확인해 보면 IDENTITY_FINISH 를 원소로 갖는 Set임을 알 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/c77sE6/btsDuyw7MUC/y4lnkRl1PN65LSdtLixIlk/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDENTITY_FINISH 이면 finisher()를 호출할 필요가 없으므로 toList()에서도 finisher()를 별도로 넘겨주지 않고 Stream.collect()에서도 결과 container를 바로 최종 반환 타입으로 형반환 후 반환하게 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/baDoMd/btsDogZroVL/1OKT3E2XJKNf6pfDvQGK50/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;h2 id=&quot;groupingby-동작-과정&quot; data-ke-size=&quot;size26&quot;&gt;groupingBy() 동작 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 드디어 마지막으로, Collectors에 작성되어 있는 groupingBy() 메서드의 동작 과정을 다시 살펴보자. 먼저 이 글의 맨 위에 있는 (처음에 봤던) groupingBy() 코드는 아래와 같다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/ycXxC/btsDrg4Ywgu/VvFy9qiTxTmYroPQJdp710/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드는 mapFactory 파라미터에 HashMap::new 를 넣어서 다시 아래의 시그니쳐를 갖는 groupingBy() 메서드를 호출한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlrfgs/btsDqi953ab/T35k1D9znnXp8Fdl04w2nK/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드의 전체 코드는 아래와 같다. 여기서 map의 value를 만들기 위한 downstream 도 Collector 타입이라는 것이 가장 중요한 부분이라고 생각된다. 즉, &lt;b&gt;map을 Collector를 만들기 위해 또 다른 Collector를 사용&lt;/b&gt;하고 있는 것이다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/yBcUI/btsDrGPW0I7/eb7K5JmNewDHTmTXSOOd50/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 downstream Collector를 사용하는 윗부분과 새로운 Collector를 만드는 아랫 부분으로 나누어서 정리해 보았다. 먼저, 윗부분을 보면&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPWOHo/btsDrFDsyUo/kdcE4gm7cYi8hY3mq6cK8K/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;downstream의 supplier, accumulator를 변수로 뽑아둔다&lt;/li&gt;
&lt;li&gt;입력 T를 통해 &lt;b&gt;map을 만들기 위한 accumulator를 정의&lt;/b&gt;한다
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;먼저 classfier를 통해 입력 T에서 key를 추출하고&lt;/li&gt;
&lt;li&gt;Map m에 이미 key에 해당하는 value가 있으면 사용하고, 없다면 downstream에서 supplier()를 통해 map의 value에 해당하는 container를 생성한다&lt;/li&gt;
&lt;li&gt;downstreamAccumulator를 사용해 map value인 container에 입력 t를 누적한다&lt;/li&gt;
&lt;/ol&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;img src=&quot;https://blog.kakaocdn.net/dn/bwpc03/btsDrovgKUl/SNKF3E4gbWqMKJUfKjZiSk/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;병렬로 연산이 수행될 경우에 대비해 동일한 key에 대한 여러 개의 map value를 merge할 때 사용할 merger를 만든다&lt;/li&gt;
&lt;li&gt;mapFactory의 타입을 key K, value A에 맞게 변환해준다&lt;/li&gt;
&lt;li&gt;Collector의 구현체를 생성한다
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CH_ID의 경우 finisher 없이 바로 구현체를 생성하고,&lt;/li&gt;
&lt;li&gt;반환 타입이 결과 container와 달라지는 CH_NOID 의 경우 finisher를 만들어서 같이 전달한다&lt;/li&gt;
&lt;/ol&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;정리해 보면, classfier 를 사용해 입력 T에서 key를 추출하고 downstream Collector의 supplier와 accumulator를 사용해 입력 타입 T를 원하는 타입으로 변환하여 map에 누적하는 &lt;b&gt;새로운 accumulator를 만든다&lt;/b&gt;. 그리고, mapFactory와 새로운 accumulator, merger를 갖는 새로운 (map을 만드는) Collector의 구현체를 반환한다. 여기서 반환된 Collector는 Stream.collect()에서 호출되어 실제 Map이 만들어지게 될 것이다.&lt;/p&gt;
&lt;h3 id=&quot;맨-처음-예시&quot; data-ke-size=&quot;size23&quot;&gt;맨 처음 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 처음에 봤던 groupingBy() 메서드의 API 문서를 다시 살펴보면,&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckpfyQ/btsDp7npd16/YApuZrWsVSaVIrxVhlwvDK/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;groupingBy()의 downstream 인자에 mapping() 메서드의 결과를 전달하는 것을 볼 수 있다. 이 메서드 또한 Collectors에 위치한 메서드로&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/DznXR/btsDs3D4Mis/l8sbTMTh0sdPhIlM8cBGBK/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용을 보면 또다시 Collector를 중첩해서 사용하는 것을 알 수 있다 (..ㅎㅎ) 근데 사실 간단하다. 입력 T를 어떻게 value의 원소로 매핑할지를 결정하는 mapper와 mapping된 값을 맵의 value(일반적으로 Collection)에 어떻게 누적할지를 결정하는 downstream Collector로 구성되어 있다.&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;downstream의 supplier()를 통해 결과 container를 만들고 거기에 mapper.apply(t)를 호출하면서 계속해서 매핑된 입력을 누적하는 accumulator 람다식을 갖는 Collector 구현체를 반환하게 된다.&lt;/p&gt;
&lt;h2 id=&quot;응용&quot; data-ke-size=&quot;size26&quot;&gt;응용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collector의 구조를 이해했으니 이제 이걸 응용할 수 있다! 예를 들어 기존에는 Collectors.mapping 메서드를 사용해서 매핑된 입력을 list에 누적하여 map을 만들었지만, 그냥 단순하게 key 별로 개수를 세도록 할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    List&amp;lt;Person&amp;gt; people = List.of(
        new Person(10, &quot;나&quot;),
        new Person(20, &quot;너&quot;),
        new Person(10, &quot;얘&quot;),
        new Person(10, &quot;쟤&quot;)
    );

    Map&amp;lt;Integer, Integer&amp;gt; map = people.stream()
        .collect(Collectors.groupingBy(Person::getAge,
            Collector.of(() -&amp;gt; new int[1], (arr, i) -&amp;gt; arr[0] += 1, (arr1, arr2) -&amp;gt; {
                arr1[0] += arr2[0];
                return arr1;
            }, arr -&amp;gt; arr[0]))
        );

    System.out.println(map); // {20=1, 10=3}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 key 별 개수를 세준다. 그런데 사실 이거랑 거의 똑같은 메서드가 이미 Collectors에 정의되어 있다 ㅋㅋ&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBaJlY/btsDrhJzTAu/p7QnMT9BlwDgI3F1fcCLGK/img.png&quot; alt=&quot;Untitled&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메서드 말고도 엄청 다양한 편의 메서드가 존재한다. 그래서 사실 Collectors를 직접 구현해서 사용할 일은 없을 것 같지만 구조를 이해하고 어떻게 동작하는지 확실히 정리해둘 수 있어 좋았다.&lt;/p&gt;
&lt;h2 id=&quot;참고-자료&quot; data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html&quot;&gt;https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/article&gt;</description>
      <category>개발 공부/Java</category>
      <category>Collector groupingBy()</category>
      <category>groupingBy mapping</category>
      <category>groupingBy()</category>
      <category>Java Collector</category>
      <category>자바 Collector</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/159</guid>
      <comments>https://gmelon.dev/159#entry159comment</comments>
      <pubDate>Sun, 14 Jan 2024 23:03:10 +0900</pubDate>
    </item>
    <item>
      <title>2023년 회고</title>
      <link>https://gmelon.dev/156</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벌써 2023년이 몇 시간 안 남았다. 1년이 어떻게 갔는지 잘 모르겠다. 시간이 진짜 빠르다는 생각을 요즘 정말 많이 하는 것 같다. 며칠 전부터 올해를 돌아보며 그래도 열심히 했어! 아니야 난 너무 게을렀어 를 왔다 갔다 하며 ㅋㅋ   회고 쓰는 걸 미루고 있었다. 하지만 미뤄봤자 엄청난 글이 써지지 않는 걸 알기에.. 올해가 가기 전에 올해를 정리해보려고 한다.&lt;/p&gt;
&lt;h2 id=&quot;졸업&quot; data-ke-size=&quot;size26&quot;&gt;졸업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 초에 졸업을 했다. 고졸 탈출~~ 과 동시에 어엿한 백수가 되었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/NJzGk/btsCSbDp0ry/SPYCbBmKKhikkQt2MIUEe0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학교 생활에 대해선 할 얘기가 많지만 정리하면 후회없이 열심히 살았다는 것이다. 전공 수업도 열심히 듣고 과제도 열심히 했다. 3학년 때는 거의 매일 울면서 타자를 치며 밤을 새웠던 기억이 난다   그리고 운이 좋게도 수석으로 과를 졸업할 수 있게 되었다. 이걸 목표로 공부한 건 아니지만 그래도 그동안 노력한 결과에 대한 보답을 받은 것 같아 엄청 기분이 좋았다. 뭔가 한 단락을 잘 마무리했다는 느낌을 받았고 앞으로 또 열심히 해나갈 힘을 얻었다.&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;h2 id=&quot;스터디&quot; data-ke-size=&quot;size26&quot;&gt;스터디&lt;/h2&gt;
&lt;h3 id=&quot;자바-기초-스터디&quot; data-ke-size=&quot;size23&quot;&gt;자바 기초 스터디&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;졸업 전인 1월부터 자바 스터디를 시작했다. 자바를 주력으로 쓰고 있으면서도 자바 문법에 대해 잘 모르고 있다고 느껴서 전체적으로 정리를 확실하게 해두고 싶었다. 딱 원하는 커리큘럼의 스터디를 찾기가 어려워 내가 직접 사람을 모으고 스터디를 만들었다. 이런걸 잘해보지 않아 걱정도 됐는데 생각보다 금방 사람들이 모여서 신기했다. 나에게 맞는 스터디를 찾아 들어가는 건 어려워도 내가 스터디를 만들고자 하면 금방 사람들이 모인다는 것을 이때 알게 되었고 이때부터 필요하면 스터디를 찾기보다 내가 직접 만드는 습관이 생겼다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/1L3yj/btsCOGD91NB/e42tLtKHQDSGgpVy5sh9TK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/2023-java-study/whiteship-java-study&quot;&gt;github 링크&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;a href=&quot;https://github.com/whiteship/live-study/issues?q=is%3Aissue+is%3Aclosed&quot;&gt;자바 스터디&lt;/a&gt; 를 참고해서 진행했다. JVM과 데이터 타입부터 제네릭, 람다식까지 15주 차 동안 매주 어떤 주제와 세부 주제를 공부해야 하는지 명시되어 있고 주차별 피드백 영상도 유튜브에 올라와있어서 우리끼리 진행한 스터디였지만 과거 백기선 님이 리드하셨을 때 참여했던 것처럼 스터디를 진행할 수 있었다. 자바 기초 스터디 커리큘럼을 찾고 계시는 분이 계시다면 강추드린다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/x3pS7/btsCN4ypDgU/bm7kcDG4wytgqrH8WSKF50/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디가 끝나고 계산해보니 당시에도 스터디 완주율이 20%에 불과했었는데 우리끼리 스터디를 완주한 것에 뿌듯함을 느껴 팀원들에게 공유했던 기억이 난다 ㅋㅋㅋ&lt;/p&gt;
&lt;h3 id=&quot;이펙티브-자바-스터디&quot; data-ke-size=&quot;size23&quot;&gt;이펙티브 자바 스터디&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 스터디 하나를 무사히 끝내고 삘을 받아서 그대로 이펙티브 자바에도 도전하게 되었다. 무려 90개의 아이템을 보유한 무시무시한 책이었지만 천천히 읽어보자고 하고 시작했다. 역시 난이도가 꽤 있는 편이라 확실하게 정리하면서 넘어가고 싶어 헷갈리는 부분을 issue에 남기고 아이템별 '리드'를 정해서 리드가 각 issue에 대한 답변을 책임지고 남기도록 규칙을 만들었다. (이펙티브 자바 스터디 진행 방식은 이미 유명한 &lt;a href=&quot;https://javabom.tistory.com/70&quot;&gt;자바 봄&lt;/a&gt;이라는 스터디를 많이 참고했다)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAJRsv/btsCUA3Fgds/wLZzLQd4b2AxaeQC4ifXf0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/2023-java-study/book-study/issues&quot;&gt;issue 링크&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;3월부터 9개월간 진행했고 얼마 전에 90개의 아이템을 모두 읽었다. 나름대로 어려운 부분은 토론도 하고 예제 코드도 함께 만들어가며 읽었지만 여전히 이해가 되지 않는 부분이나 어려운 부분도 남아있다. 그래도 어떤 아이템이 있고 무슨 얘기를 하는지 훑어봤으니 다음에 필요할 때 좀 더 쉽게 살펴보고 이해할 수 있지 않을까? 생각한다.&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;이펙티브 자바는 아이템별로 무엇무엇해라!   또는 뭐뭐하지마라!   (단호) 라고 확실하게 얘기해 주는 것들이 많아서 개발할 때 기준을 잡는데 도움이 많이 되었다. 또 존재하는지도 몰랐던 자바의 API나 기능에 대해 알게 되고 그것들을 분석해 볼 기회들이 많아서 자바 자체에 대한 공부도 많이 되었다. 자바와 조금 더 친해진 느낌이 든달까.. 하하 이젠 스스로를 자바 개발자라고 조금은 자신 있게 말할 수 있을 것 같다.&lt;/p&gt;
&lt;h3 id=&quot;알고리즘-스터디&quot; data-ke-size=&quot;size23&quot;&gt;알고리즘 스터디&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 스터디와 별개로 알고리즘 스터디도 봄 정도부터 진행했다. 4명이서 시작했고 지금도 매주 문제를 풀어오고 함께 풀이하고 토의하는 방식으로 스터디를 진행하고 있다. 나 포함 다들 처음에는 유형에도 익숙하지 않은 상태였지만 몇 달간 이론과 유형별 기본 문제 풀이 과정을 거쳐 지금은 골드 중위권 문제 정도는 풀 수 있는 실력이 된 것 같다. 실제로 코테에 통과하는 횟수도 늘고 있고 스터디 이후에 봤던 코테에서 스터디 전보다 확연히 달라진 것을 느끼고 있다. 서로에게 도움이 되는 스터디를 해나가고 있는 것 같아서 다행이라고 생각했다.&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일로 스키장도 다녀왔다. 1년 가까이 나를 잘 이끌어준 스터디 팀원들에게 너무나 감사하다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/uBB9F/btsCMpv53eN/8Gnre7t7Gi6qjMbP3SDapk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;프로젝트&quot; data-ke-size=&quot;size26&quot;&gt;프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 해커톤에서 수상했던 프로젝트를 올 여름부터 보완하기 시작해서 얼마 전에 앱스토어에 배포까지 완료했다. 올해 가장 열심히 한 일인 것 같다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/4ReRt/btsCOzZdP4c/6eMroVeOL9Rc9s3mxRVbVk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sh-hyun.tistory.com/151&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;개발을 시작할 땐 백엔드 애플리케이션 이라는 것을 어떻게 만들기 시작해야 하는지 잘 몰랐다. 그래서 무작정 nextstep 강의를 들으며 방법을 배웠다. 짧은 주기로 코드 리뷰를 받으며 미션을 수행하는 과정에서 예외 처리, 로깅, DTO의 사용 범위, 계층 등 굉장히 많은 고민들을 만났고 이를 해결하는 과정에서 백엔드 개발에 대한 나름의 기준을 만들 수 있었다.&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;b&gt;기록하지 않으면 기록할게 없는 인생이 된다&lt;/b&gt;는 뼈아픈 명언을 되새기며 앞으로도 계속 짧게라도 기록을 해나가야겠다 다짐했다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXhWCG/btsCScCm4Xg/PlvwAIHyzG9e1B3vEl9aR1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h3 id=&quot;배포-후&quot; data-ke-size=&quot;size23&quot;&gt;배포 후&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 배포한지 2주 정도 지났다. 처음에는 써보라고 소개&lt;s&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;강매&lt;/span&gt;&lt;/s&gt;한 친구들 위주로 가입이 되었는데 앱이 스토어에 올라가서 검색으로 유입이 되다 보니 정말 신기하게도 전혀 모르는 사람도 가끔 가입을 해주고 있다. 우리 서비스에 정말 유저가 생기고 있는 것이다..!!&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/kLcHB/btsCU0HWMK3/q8Un7k08IUprMOdmlByM51/img.png&quot; alt=&quot;&quot; /&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;img src=&quot;https://blog.kakaocdn.net/dn/d2SpFQ/btsCQ4khKEG/JK5zL7huPI5nAaAsjye0Yk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 공부는 배포한 다음부터구나 생각했다. 아직 해보고 싶은 리팩토링이나 성능 개선 테스트도 많이 쌓여있고 추가, 개선하고 싶은 요구사항도 많다. 다행인 건 이것들이 짐으로 느껴지지 않는다는 것이다. 오히려 지금 당장 모두 하지 못한다는 것이 답답하고 앞으로 해나갈 생각에 설렌다. 이것들을 하나씩 해결하며 더 나아질 앱의 모습과 성장할 나 자신이 기대된다!&lt;/p&gt;
&lt;h2 id=&quot;게으름과-조급함&quot; 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;img src=&quot;https://blog.kakaocdn.net/dn/cfeviF/btsCMojJdoK/CNEUOKvu1GchgvS4EFLiWk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(엄청 공감됐던 TED 강의, 대충 미루는 사람이 마감 괴물 때문에 일을 겨우 끝낸다는 얘기 중  )&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;img src=&quot;https://blog.kakaocdn.net/dn/lwvz3/btsCWKkya9T/Eg9tVp4eCx51aGfB6mGFJk/img.jpg&quot; alt=&quot;&quot; /&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;h2 id=&quot;싸피&quot; data-ke-size=&quot;size26&quot;&gt;싸피&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 지원했고 다행히 11기에 합격했다!&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/tkRZ1/btsCOE0EZky/dbVQjGnFJ19vdMZziERAqk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 싸피를 통해 얻고자 하는 것은 크게 3가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;뛰어난 동료들과 함께 배우기&lt;/li&gt;
&lt;li&gt;기상, 취침시간을 비롯해 공부하는 환경 조성&lt;/li&gt;
&lt;li&gt;혼자서 취업을 준비한다는 조급함 해소&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 지원 횟수가 많진 않지만 운 좋게 &lt;a href=&quot;https://sh-hyun.tistory.com/154&quot;&gt;최종 면접까지 경험해 본&lt;/a&gt; 기업도 있었는데 서류부터 최종 면접까지 한 사이클을 돌며 지금 내가 무엇이 부족하고 어떤 식으로 준비를 해야 할지에 대해 알 수 있는 좋은 경험이었다. 그리고 싸피의 커리큘럼이 내가 원하는 부분을 많이 채워줄 수 있을 것이라는 생각이 들었다. 당장 모레부터 시작인데 한번 또 최선을 다해볼 계획이다.&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내년 목표는 &lt;code&gt;게을러지지 않고 기록하며 긍정적으로 살기&lt;/code&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;그리고 운동도 다시 시작할 거다! 헬스 하는 친구들한테 말하면 다 신기해하던데 나는 웨이트 트레이닝을 하면 스트레스를 (엄청) 받고 (pt 20회 끊어놓고 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;/article&gt;</description>
      <category>기록/후기, 회고</category>
      <category>2023 회고</category>
      <category>개발자회고</category>
      <category>회고</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/156</guid>
      <comments>https://gmelon.dev/156#entry156comment</comments>
      <pubDate>Sun, 31 Dec 2023 21:20:19 +0900</pubDate>
    </item>
    <item>
      <title>2023 하반기 CJ올리브네트웍스 지원 후기</title>
      <link>https://gmelon.dev/154</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;23년 9~11월 사이에 진행된 CJ올리브네트웍스의 신입 공채 경험을 정리했다. 백엔드 개발자로 취업 준비를 하며 사실상 처음으로 최종 면접까지 갔던 값진 경험이었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/vNTd9/btsCRh3TkXZ/ZOEyrxcyQeNDCDEDekYYIk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;서류&quot; data-ke-size=&quot;size26&quot;&gt;서류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 질문은 3가지였는데, 아래와 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/w1gPl/btsCQ4p0LQL/fzRDLRu6ECRnuRZ1o8zqEk/img.png&quot; alt=&quot;&quot; /&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번 아이디어 제안 항목에서 고민을 많이 했다. 나한테 가장 익숙한 CJ 계열사는 cgv라서 영화와 관련된 아이디어를 it 기술과 접목하여 작성했다. 다시 읽어보니 이게 무슨 말이지? 싶다 ㅎ.. 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;마지막 포트폴리오는 README가 작성된 프로젝트 레포가 있어서 해당 깃헙 레포 url을 제출했다.&lt;/p&gt;
&lt;h2 id=&quot;test&quot; data-ke-size=&quot;size26&quot;&gt;TEST&lt;/h2&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmYF3Q/btsCQ3xQCq0/rK7d3qFFRB9rKniSTXNIzk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 스터디를 마치고 타슈(대전의 공유 자전거)를 타고 집에 가고 있었는데 문자가 와서 화들짝 놀라 멈춰서 확인했던 기억이 난다. 코딩테스트는 온라인으로 진행되었고 크게 어렵지 않은 수준이었던 것 같다.&lt;/p&gt;
&lt;h2 id=&quot;1차-면접--cj-cft&quot; data-ke-size=&quot;size26&quot;&gt;1차 면접 + CJ CFT&lt;/h2&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdpynA/btsCLr01Xf7/2jUFGkcKNAUHG0BC8cr6Lk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코테에 합격하고 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;1차 면접은 온라인으로 진행되었다. Bridge Office라는 cj에서 개발한 (아마 올네에서 만들었겠지..?) 메타버스 오피스에서 면접을 봤다. 나중에 본사에 가서 알게 된 거지만 진짜 회사랑 완전 똑같이 만들어놔서 너무 신기했다 ㅋㅋㅋ 코로나가 심했을 땐 이 플랫폼 통해서 온라인 인턴십도 진행했었다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/DKe4U/btsCN1nfqNY/r7Om7iZ67Eu4cgDCslpf30/img.png&quot; alt=&quot;&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;플랫폼에 접속해 개인 직무 면접과 토론 면접을 진행하고 정해진 시간 내에 별도로 CJ CFT 라고 하는 인성 검사도 이 단계에서 같이 진행했다. 인성 검사가 생각보다 어렵다고 얘기는 많이 들었는데 실제로 해보니 정말로 어떤 걸 골라야 유리한 건지 판단하기가 어려운 문항들이 많았다. 그래서 나중에는 그냥 떠오르는 대로 솔직하게 답했던 것 같다. 답을 꾸며내려고 하면 오히려 일관성이 떨어져서 그냥 처음부터 떠오르는 대로 선택하는 게 좋지 않을까 생각했다.&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;h2 id=&quot;2차-면접&quot; data-ke-size=&quot;size26&quot;&gt;2차 면접&lt;/h2&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/EuD2w/btsCRgqn3WV/ukf9UwsEs9KsbhEAHzPq0K/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1차 면접에 합격했다! 사실 1차 면접 때까지는 별생각 없었는데 최종 면접이라니 갑자기 엄청 실감이 났다. 이것만 통과하면 취준이 끝나는 건가? 생각하니 약간 허무하기도 하고 떨리기도 하고 그랬다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk5p8n/btsCQpOEcrZ/RuvWisajKQcS89QUm9PqCk/img.png&quot; alt=&quot;&quot; /&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로 40분 정도 진행되었다. 인상 깊었던 것은 분위기가 너무너무 편안했다는 것이다. 정말 편하게 이야기할 수 있는 분위기를 만들어주셔서 부담 없이  말할 수 있었다.  임원분들이 의식적으로 노력해 주시는 게 느껴져서 너무 감사했다. 지금 생각해 보면 방 조명도 형광등이 아니라 약간 따뜻한 조명이었는데 의도된 게 아니었을까..? 그래서 면접 전보다 후에 입사하고 싶은 마음이 엄청 커졌었다. 질문 내용은 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;면접 전후로 맛밤 같은 제일제당 간식류를 제공해 주셨는데 뭔가 CJ 계열사에 CJ 간식들 쭉 진열되어 있는 거 좀 귀여웠다.&lt;/p&gt;
&lt;h2 id=&quot;결과&quot; data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpHfvs/btsCIGYLQKs/xn0eAUUYtXSSAklylLoWBk/img.png&quot; alt=&quot;&quot; /&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;/article&gt;</description>
      <category>기록/후기, 회고</category>
      <category>cj올리브네트웍스</category>
      <category>CJ올리브네트웍스 면접</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/154</guid>
      <comments>https://gmelon.dev/154#entry154comment</comments>
      <pubDate>Fri, 29 Dec 2023 01:54:14 +0900</pubDate>
    </item>
    <item>
      <title>테스트에서만 @Async 적용되지 않도록 하기</title>
      <link>https://gmelon.dev/153</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐️ 방법 매우 간단함 주의..&lt;/p&gt;
&lt;h2 id=&quot;문제-상황&quot; data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sh-hyun.tistory.com/143#3-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC&quot;&gt;알림 발송 로직을 비동기로 처리&lt;/a&gt;하고 테스트를 수행하려니 아래와 같이 알림 발송을 검증하는 테스트가 깨지는 문제가 생겼다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/qxC9A/btsCMzRuSaS/mGR32YjfGIinAOrZ4Jx4y0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 비동기 메서드인 &lt;code&gt;notificationService.send()&lt;/code&gt;가 완료되기 이전에 값이 생성되었는지를 검증하려고 했기 때문이다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3irlZ/btsCMx7gnZS/hJeXVNZYuNB3kkMDDHz0P1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 큰 문제로, 비단 알림 발송 자체를 검증하는 테스트 뿐만 아니라 다른 테스트에서도 &lt;b&gt;해당 테스트가 수행하는 로직이 특정 작업을 마치고 알림 발송을 수행하는 경우&lt;/b&gt; 동일하게 비동기 로직이 수행되어 테스트가 깨지고 있었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj7F7V/btsCKSqqcMH/h6KljXk7ChQr65t9ZVfJh1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 예시로, 위의 경우 테스트 완료 후 수행하는 &lt;code&gt;reset.sql&lt;/code&gt; 에서 알림 테이블을 지울 때 존재하지 않던 알림이 (비동기로 실행되어) 회원 테이블을 지울 때 존재하게 되는 상황이다. 때문에 외래키 무결성 위반으로 오류가 발생했다.&lt;/p&gt;
&lt;h4 id=&quot;resetsql&quot; data-ke-size=&quot;size20&quot;&gt;reset.sql&lt;/h4&gt;
&lt;img src=&quot;https://github.com/gmelon/plango-backend/assets/33623106/724dc2f7-17f5-48ae-964c-699712ecd954&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;해결-방법&quot; data-ke-size=&quot;size26&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async를 사용하려면 @EnableAsync 어노테이션을 설정 파일에 달아주어야 한다. 기존에는 아래와 같이 WebConfig에 같이 달아주고 있었는데,&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/MaLZi/btsCKpICivU/QKVEhEOlHe3gkzt3qgLGY1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 AsyncConfig로 분리해주고 &lt;b&gt;test가 아닌 프로필에서만&lt;/b&gt; 빈으로 등록되도록 해주었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/kBrkk/btsCMz43fqB/N0CFBgsoa10CzJTjUKCmc0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에도 테스트는 아래와 같이 test 프로필로 실행되고 있었으므로 이렇게 해주는 것만으로 문제를 해결할 수 있었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/niuFH/btsCKsk5i7j/xHDbr17YdDNFUN66YplBS1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 알림 발송 메서드가 동기로 실행되어 테스트가 성공하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/QaVvw/btsCGarEKXV/Alg4xckpQNkirmKFhkAVK0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/article&gt;</description>
      <category>개발 공부/Spring</category>
      <category>@Async</category>
      <category>@Profile</category>
      <category>EnableAsync</category>
      <category>비동기테스트</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/153</guid>
      <comments>https://gmelon.dev/153#entry153comment</comments>
      <pubDate>Wed, 27 Dec 2023 23:39:04 +0900</pubDate>
    </item>
    <item>
      <title>[플랭고] 프로젝트 앱스토어 / 플레이스토어 배포 회고</title>
      <link>https://gmelon.dev/151</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 여름부터 개발해 왔던 &lt;b&gt;장소기반 일정관리 서비스 '플랭고'&lt;/b&gt;를 드디어 앱스토어와 플레이스토어에 배포했다!&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7iogJ/btsCzKMcFbL/KXbRVkKWGNQEnuFzb631Ik/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 생각했던 것보다는 이런저런 일 때문에 늦어졌지만 올해 안에 개발 시작부터 배포까지 한 사이클을 완료할 수 있어 굉장히 의미 있는 경험이었다. 비슷한 상황의 누군가에게 조금이라도 도움이 되었으면 좋겠다는 마음을 담아 개발 시작 단계부터 배포 과정까지 기억나는 일들을 정리해 봤다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&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://apps.apple.com/kr/app/%ED%94%8C%EB%9E%AD%EA%B3%A0-%EC%9E%A5%EC%86%8C-%EA%B8%B0%EB%B0%98-%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC/id6444399139&quot;&gt;AppStore 링크&lt;/a&gt; / &lt;a href=&quot;https://play.google.com/store/apps/details?id=dev.gmelon.plango&quot;&gt;PlayStore 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;프로젝트-시작-계기&quot; data-ke-size=&quot;size26&quot;&gt;프로젝트 시작 계기&lt;/h2&gt;
&lt;h3 id=&quot;이-행운의-프로젝트는-대전의-한-해커톤에서-시작되어&quot; data-ke-size=&quot;size23&quot;&gt;이 행운의 프로젝트는 대전의 한 해커톤에서 시작되어&amp;hellip;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 플랭고는 작년 가을 교내 &lt;a href=&quot;https://sh-hyun.tistory.com/47&quot;&gt;앱 개발 해커톤인 '콜라톤'&lt;/a&gt;에서 시작된 프로젝트다. 2022년 9월부터 약 두 달간 팀원 4명이서 열심히 프로젝트를 진행했고 최종 발표 결과 해커톤에서 우수상을 수상할 수 있었다.&lt;/p&gt;
&lt;h3 id=&quot;백엔드-없이-완성된-앱&quot; data-ke-size=&quot;size23&quot;&gt;백엔드 없이 완성된 앱&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 사실.. 해커톤 종료 당시에 &lt;b&gt;이 앱에는 백엔드가 존재하지 않았다..!&lt;/b&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/DNUxR/btsCv2unhUU/Fre2nZiXTui7JvE0CsK2P0/img.png&quot; alt=&quot;&quot; /&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;원래는 백엔드 + 프론트엔드로 구성된 앱을 만들고자 했지만 당시 프론트엔드를 담당해 주기로 했던 팀원에게 문제가 생겨 나 혼자서 백/프론트를 모두 개발해야 했었다. 2주 안에 전혀 해보지 않은 앱 프론트를 학습하면서 기획 팀원들이 제시한 요구사항을 만족하는 앱을 개발하기는 무리라고 판단했고, 보여지는 부분이 중요한 해커톤인만큼 우선 프론트에 집중하기로 결정했다.&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;a href=&quot;https://www.inflearn.com/course/%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%95%B1%EA%B0%9C%EB%B0%9C-%EC%99%84%EC%84%B1&quot;&gt;플러터 강의&lt;/a&gt;를 구매해 (플러터 입문 강의로 강추드립니다!!) 열심히 공부하면서 밤을 새워가며 개발했고 다행히 기간 내에 개발을 완료할 수 있었다.&lt;/p&gt;
&lt;h3 id=&quot;이제-백엔드와-연동해보자&quot; data-ke-size=&quot;size23&quot;&gt;이제 백엔드와 연동해 보자!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 해커톤이 끝나고 자연스럽게 &lt;b&gt;프론트엔드만으로 구성되어 있던 플랭고의 로직들을 백엔드로 옮겨보자&lt;/b&gt;는 목표가 생겼다. 그래서 수상 이후에야 본격적으로 백엔드 개발을 시작했다.&lt;/p&gt;
&lt;h2 id=&quot;근데-백엔드는-어케-만드는겨&quot; data-ke-size=&quot;size26&quot;&gt;근데 백엔드는 어케 만드는겨&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 막상 백엔드를 만들어보자고 생각하니 어떻게 하는 건지 전혀 감이 잡히질 않았다. 지금까지 백엔드를 공부할 때 김영한님 강의에 포함된 예제를 따라 작성하고 돌려본 게 다인데 막상 내 프로젝트를 만들어보려니 전혀 손이 움직이지 않았다. 직접 만들어보지 않으면 아무것도 남지 않는다는 걸 이때 많이 깨달았다.&lt;/p&gt;
&lt;h3 id=&quot;넥스트스텝-교육&quot; data-ke-size=&quot;size23&quot;&gt;넥스트스텝 교육&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 고민하다가 넥스트스텝의 &lt;a href=&quot;https://edu.nextstep.camp/c/X1pbG30l&quot;&gt;학습 테스트로 배우는 Spring&lt;/a&gt; 과정을 수강하게 됐다. 이유는 작년에 들었던 넥스트스텝 자바 클린코드 과정이 너무나 만족스럽고 배운 게 많았기 때문이기도 하고 특히 이 과정은 스프링을 공부하는데 단순히 이론을 강의로 전달해 주는 게 아니라 &lt;b&gt;레퍼런스나 자료를 읽고 나서 학습 테스트를 적극 활용해 스스로 공부하는 법을 알려주기 때문&lt;/b&gt;에 프로젝트를 만들기 위한 지식을 얻는 데에도, 앞으로 개발자로서 계속 공부를 해나가는 데에도 도움이 될 거라고 생각했기 때문이다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXdxmi/btsCwOWOKTO/wkX46zNNZSYPazYCKuviZ1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시큐리티를 사용하지 않고 직접 인터셉터, argument resolver 등을 사용해 로그인을 구현하는 첫 번째 미션을 시작으로 마지막 지하철 요금 계산 API 서버 개발 미션까지 코드 리뷰 방식으로 피드백을 받으며 많은 것들을 배울 수 있었다. 테스트, DTO, 예외 처리 등등 &lt;b&gt;여러 가지 고민들을 만나고 해결하는 과정에서 어떻게 개발을 해야 할지에 대해 나름의 기준들을 세워나갈 수 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 id=&quot;개발-과정&quot; data-ke-size=&quot;size26&quot;&gt;개발 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NEXTSTEP 교육을 마치고 본격적으로 개발을 시작했다. 기존에 플러터로 앱을 개발하며 느꼈던 점이 &lt;b&gt;목표가 있는 개발을 하니 개발과 이에 필요한 학습을 모두 효과적으로 진행할 수 있다는 것&lt;/b&gt;이었다. 때문에 백엔드를 개발할 때도 이러한 루틴을 최대한 활용할 수 있도록 노력했다.&lt;/p&gt;
&lt;h3 id=&quot;학습-루틴-만들기&quot; data-ke-size=&quot;size23&quot;&gt;학습 루틴 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 아래와 같은 사이클로 개발을 하게 되었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/qZcl8/btsCwLsmj5S/uJlFkf3kYgomyN2Cf5s2n1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 페이징에 대해 학습하고 싶다고 하면 &lt;code&gt;페이징 기능을 제공하는 API&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;프로젝트의 기능 개발&lt;/code&gt; 이라는 목표가 있으니 학습이 지루하지 않고, 또 학습한 내용을 바로 코드에 적용해 볼 수 있어서 재밌게 개발했던 것 같다. 무엇보다 &lt;b&gt;이론만 계속해서 학습하며 준비될 때까지 기다리지 않고&lt;/b&gt; 일단 개발을 시작할 수 있다는 점이 잘하고 싶어서 일을 미룬다는 내가 가진 단점을 극복하는데 많은 도움을 주었던 것 같다.&lt;/p&gt;
&lt;h3 id=&quot;문제-해결&quot; data-ke-size=&quot;size23&quot;&gt;문제 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 개발하며 발생했던 문제들과 나름대로 도출해 낸 해결 방법들은 틈틈이 블로그에 정리해 두었다. 은근 비슷한 상황을 다시 만나는 경우가 많아서 그때 어떤 방향으로 고민했더라? 를 볼 수 있는 게 꽤나 도움이 되었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwhQo0/btsCxIaFzio/xvqtsvivwD5W1G754tiJl0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sh-hyun.tistory.com/search/%ED%94%8C%EB%9E%AD%EA%B3%A0&quot;&gt;플랭고 문제해결기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;testflight-무중단배포-슬랙-알림&quot; data-ke-size=&quot;size23&quot;&gt;TestFlight, 무중단배포, 슬랙 알림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹이 아니라 앱이다 보니 단순히 주소를 통해서는 팀원들에게 개발 중인 프로젝트를 공유할 수가 없었다. 찾아보니 애플의 TestFlight라는 플랫폼을 사용하면 테스터들을 대상으로 배포 기능을 사용할 수 있다고 했다. 다행히 기획 팀원들 모두가 아이폰을 사용하고 있어서 이 플랫폼을 사용해 팀원들에게 개발 중인 앱을 배포하고 편하게 피드백을 전달받을 수 있었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8ymLc/btsCywnqqRE/WiCBgzkxUjMhMXWRaHHLs1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(TestFlight는 위 이미지처럼 테스터들이 기기에서 캡처 후 바로 피드백을 보낼 수 있는 기능을 제공한다)&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;앱을 배포하기 위해 백엔드를 aws의 ec2에 배포하고 s3나 rds와 연동하면서 배포, 테스트 자동화나 무중단배포도 경험할 수 있었다. 처음 구축할 땐 머리가 너무 아팠지만 한번 해두니 확실히 이후 배포 때마다 엄청 편해져서 ci/cd, 배포 자동화 등이 왜 필요한지 몸소 체감할 수 있었다.&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;b&gt;너무너무&lt;/b&gt; 멋있어 보여서 꼭 해보고 싶었는데 슬랙 Webhook을 사용해서 배포 성공 또는 실패 시 슬랙 알림도 발송되도록 만들었다. 어렵진 않았지만 개인적으로는 배포 자동화나 무중단 배포보다 이게 더 뿌듯했다 ㅋㅋㅋㅋ 뭔가.. 진짜 개발자가 된 기분..&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/JuhKf/btsCwQG4YhJ/oS6HeDS1oV0vr6KdTGe250/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h3 id=&quot;프론트엔드-개발&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러터는 재밌었지만 가끔 뭐가 잘 안 돼서 시간을 많이 잡아먹으면 백엔드도 할게 많은데 프론트엔드에 시간을 쓰고 있다는 게 조급하게 느껴지기도 했다. 근데 또 한편으론 사용자가 쓰게 될 화면을 직접 만든다는 설렘이나 보람 덕분에 프로젝트가 지루해지거나 문제가 잘 안 풀릴 때 가장 큰 동기부여를 준 요소기도 했어서 좋은 선택이었다고 생각한다. (하지만 다음 프로젝트부턴 꼭 팀으로 개발할 거다  )&lt;/p&gt;
&lt;h2 id=&quot;앞으로의-계획&quot; data-ke-size=&quot;size26&quot;&gt;앞으로의 계획&lt;/h2&gt;
&lt;h3 id=&quot;성능-개선-리팩토링&quot; data-ke-size=&quot;size23&quot;&gt;성능 개선, 리팩토링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 완성되어 배포가 된 플랭고 프로젝트에 이것저것 해보며 계속 백엔드를 학습할 계획이다. 예를 들면 지금은 테이블에 인덱스가 거의 걸려있지 않은데 이번에 스터디에서 MySQL의 실행계획이나 옵티마이저 등을 공부하며 order by나 group by 등에서도 인덱스가 성능에 영향을 준다는 것을 알게 되었다. 이에 테이블에 인덱스를 걸어서 성능 측정/최적화를 하고 정리해 볼 생각이다. 이거 말고도 코드를 좀 더 리팩토링하거나 테스트 코드를 개선해 보고 정리할 계획이다.&lt;/p&gt;
&lt;h3 id=&quot;로깅&quot; data-ke-size=&quot;size23&quot;&gt;로깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 실제 서비스를 배포해 보니 로그를 남기고 있다고 하더라도 로그가 적절하게 남겨지지 않으면 오류가 발생했을 때 오류가 어디서 어떻게 발생한 건지, 어떻게 재현할지에 대해 아무런 정보를 얻지 못하는 것을 경험하게 되었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/W3SZx/btsCzTWyg5R/xWmueQuDvXLxiJu5QIHko1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 로그만 보면 어떤 URL로 잘못된 POST 요청이 들어온 건지 당최 알 수가 없다)&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;h3 id=&quot;기능-개선&quot; data-ke-size=&quot;size23&quot;&gt;기능 개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 스토어에 배포되고, 주변 사람들에게 &amp;lsquo;스토어에 플랭고 검색해서 함 써봐~~(뿌듯)&amp;lsquo; 라고 말하고 다녔다. 확실히 웹이 아닌 앱이다 보니 앱스토어에서 내가 만든 앱이 검색되고, 유저의 스마트폰에 직접 설치된다는 게 되게 큰 보람이었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcAa3C/btsCv4TiRPW/XC8CghaXGiuHHOBkO35pKk/img.png&quot; alt=&quot;Alt text&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 사용해 보고 남겨준 피드백들을 읽어보며 개발자의 의도와 사용자의 입장은 정말 다르다는 걸 많이 느낄 수 있었다. 약간은 기획자의 시선으로 다시 기능을 고민하고 이 앱을 왜 사용해야 되는지에 대한 이유를 조금씩 명확하게 만들어 볼 계획이다. 10명이라도 내가 만든 앱을 계속 사용해 준다면 개발자로서 엄청난 동기부여가 될 것 같고, 그리고 실제로 유저가 있어야 실제 운영을 고려한 개발을 할 수 있을 것 같아서 되게 중요한 부분이라고 생각이 들었다.&lt;/p&gt;
&lt;h2 id=&quot;잡담&quot; data-ke-size=&quot;size26&quot;&gt;잡담&lt;/h2&gt;
&lt;h3 id=&quot;로고-만들기&quot; data-ke-size=&quot;size23&quot;&gt;로고 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 앱이다 보니 일단 로고가 있어야 등록할 수가 있는데 이게이게 생각보다 되게 쉽지 않았다. 디자이너가 없다 보니 로고를 직접 만들었어야 했는데 팀원들이랑 이것저것 논의해 봐도 딱 이렇다 할 아이디어가 떠오르지 않았고 결국 어도비의 firefly에 &lt;code&gt;귀여운 플라밍고&lt;/code&gt; 키워드로 한 백번정도 돌려 그중 가장 귀여운 플라밍고를 찾아내서ㅋㅋㅋ 로고로 사용했다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/delNux/btsCxIhn4Uo/7vvkVwFhr84k1b2S6hHdU0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(대략 34번째 진화 중인 로고)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRyR1v/btsCyIBgBwp/apNKabYs6Zhh4xqxBe8Zy0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인고의 시간을 거쳐 결국 완성된 로고. 처음엔 음? 싶었지만 보다 보니 귀여워서 지금은 쫌 맘에 든다.&lt;/p&gt;
&lt;h3 id=&quot;이용약관-개인정보처리방침&quot; data-ke-size=&quot;size23&quot;&gt;이용약관, 개인정보처리방침&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱스토어와 플레이스토어에 앱을 등록하려면 개인정보처리방침이 필수로 필요하다. 얼마 전까진 정부에서 제공하는 가이드 사이트가 있었는데 이게 최근 개인정보처리방침 개정으로 인해 사용이 제한된 상태이다. 그래서 구글에서 약관을 만들어주는 서비스를 찾아 약관의 기본적인 틀을 만들고 비슷한 앱들의 약관들을 참고해서 세부사항을 수정하는 방식으로 완성했다.&lt;/p&gt;
&lt;h2 id=&quot; &amp;zwj;♂️-끝&quot; data-ke-size=&quot;size26&quot;&gt; &amp;zwj;♂️ 끝&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱스토어 배포가 약간 올해의 숙원사업 같은 거였는데 올해가 가기 전에 배포까지 할 수 있어서 다행이다. 계속해서 업데이트하면서 동기부여를 얻고 힘들 때 자존감을 지켜줄 수 있는 재료로 사용하고 싶다. 계속 앱 깔아서 써보라고 귀찮게 굴어도 열심히 QA 해주신 친구들 너무나 감사합니다. 먼~~~ 나중에 광고 수익이 들어온다면 나눠드리겠습니다 ㅎㅎ&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;s&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;구독과 좋아요 알ㄹ &lt;/span&gt;&lt;/s&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;플랭고는 지금 앱스토어에 만나보실 수 있습니다~!  &lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://apps.apple.com/kr/app/%ED%94%8C%EB%9E%AD%EA%B3%A0-%EC%9E%A5%EC%86%8C-%EA%B8%B0%EB%B0%98-%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC/id6444399139&quot;&gt;AppStore 링크&lt;/a&gt; / &lt;a href=&quot;https://play.google.com/store/apps/details?id=dev.gmelon.plango&quot;&gt;PlayStore 링크&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/article&gt;</description>
      <category>프로젝트/[앱] 플랭고</category>
      <category>백엔드 배포</category>
      <category>앱스토어</category>
      <category>프로젝트 배포</category>
      <category>플레이스토어</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/151</guid>
      <comments>https://gmelon.dev/151#entry151comment</comments>
      <pubDate>Sat, 23 Dec 2023 00:31:38 +0900</pubDate>
    </item>
    <item>
      <title>[플랭고] 주요/부가 로직 트랜잭션 분리하기 - TransactionalEventListener와 REQUIRES_NEW</title>
      <link>https://gmelon.dev/143</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;플랭고는 친구와 함께 일정을 수정하고 장소를 추가하는 등 일정 공유 기능을 제공한다. 이때 새로운 일정에 초대되거나, 일정 초대를 수락/거절하거나 공유 중인 일정이 수정되는 등의 상황에 알림(자체 알림 목록 + 푸시)이 발송되도록 기능을 구현해두었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pdDcR/btsB3srsw6f/zrTKT5PRndUNpkO4z1Bgmk/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 알림을 발송하는 흐름은 다음과 같다. 먼저 일정 서비스에서 알림을 발송해야 하는 경우에 알림 DTO를 만들어 알림 서비스를 호출한다. 그러면 알림 서비스는 알림을 생성하고 외부 서비스인 FCM을 통해 대상 회원의 기기로 푸시 알림을 보낸다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx9KbT/btsB1Gxfxct/r5RcmmgtxWJIVN57eyoeEk/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 흐름으로 알림을 발송하면 위 그림처럼 일정 -&amp;gt; 알림 -&amp;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&gt;알림이나 외부 서비스 호출 중에 예외가 발생하면 제대로 완료된 &lt;code&gt;일정 초대&lt;/code&gt;도 롤백된다&lt;/li&gt;
&lt;li&gt;위 과정에서 가장 주요한 작업인 &lt;code&gt;일정 초대&lt;/code&gt;가 종료되어도 &lt;code&gt;알림 발송&lt;/code&gt;과 &lt;code&gt;푸시 발송&lt;/code&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;이러한 문제를 해결하기 위해 알림 서비스와 FCM 호출 서비스를 일정 서비스와 분리하기로 했다. 그런데 막상 하다보니 생각한대로 잘 안 되는 부분들이 있었고, 왜 그랬는지 문제를 해결하기 위해 시도한 방법과 결과들을 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Propagation.REQUIRES_NEW&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 트랜잭션을 분리하기 위해 @Transactional의 propagation 속성을 &lt;code&gt;Propagation.REQUIRES_NEW&lt;/code&gt;로 주는 방법을 생각했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceqME9/btsBY9fKjgh/6tScuoEQGRIyVkzeTCf4B1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설명처럼 이 속성은 작업을 수행할 때 기존 트랜잭션을 suspend하고 새로운 트랜잭션을 생성한다고 한다. 그래서 오? 그럼 새로운 트랜잭션이 만들어지니깐 알림 이후에 실행되는 로직에서 예외가 발생해도 부모 트랜잭션인 일정 서비스의 로직은 제대로 커밋되겠군. 이라고 생각했다.&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;code&gt;send()&lt;/code&gt; 메서드를 아래와 같이 변경해 주었다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
public void send(Long targetMemberId, NotificationType notificationType, NotificationArguments notificationArguments) {
    Member targetMember = findMemberById(targetMemberId);

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래와 같이 알림 서비스에서 예외를 던지도록 하고 테스트를 해보았다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AjUjM/btsBZGxMgF8/qoFENjz90FPCdiiuONLAr0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결과는 알림 서비스뿐만아니라 일정 서비스도 롤백되는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YQd42/btsBYu5Cyqk/ZRRlv5cBFRVq40V5GFAYl0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에 대한 원인은 정확히는 모르겠는데 일정 서비스를 호출하면서 새로운 트랜잭션이 생성되긴 하지만 부모 트랜잭션이 아직 커밋/롤백 되지 않고 열려있는 상태라서 자식 트랜잭션에서 문제가 생기면 부모 트랜잭션이 닫히면서 롤백되도록 하지 않았을까 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEtvJv/btsBXML6qth/xHiak2rk6NLj8BvMKYiKH0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;24. 01. 03. 수정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;위에서 propagation 속성을 &lt;code&gt;Propagation.REQUIRES_NEW&lt;/code&gt;로 설정해도 부모 트랜잭션의 롤백이 이뤄지던 이유는.. 부모 트랜잭션이 실행되는 메서드에서 try-catch를 해주지 않았기 때문이다.. 그럼 당연히 예외가 전파되면서 부모 메서드에서도 예외가 터질테니 트랜잭션이 롤백되겠지.. 바보야..&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/gmelon/plango-backend/assets/33623106/e6408fbf-5cf5-48f9-8c6d-f66b0d793490&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;위와 같이 try-catch를 해두면 (자식 트랜잭션을 Propagation.REQUIRES_NEW로 설정한 경우에) 아래와 같이 테스트가 통과하는 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/gmelon/plango-backend/assets/33623106/2b25dc47-0e9b-41d7-88dd-bedfa3526c50&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;반면, try-catch를 해주어도 propagation 속성이 &lt;code&gt;Propagation.REQUIRED&lt;/code&gt; 라면 아래와 같이 테스트에 실패하는 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/gmelon/plango-backend/assets/33623106/3faffa2d-ae95-43dd-acf1-160be7dbd5d2&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;이와 같은 현상이 발생하는 이유는 현재 propagation이 REQUIRED로 부모와 자식이 병합되고 있는데, 이 경우 아무리 try-catch로 잡아주어도 결국 하나의 트랜잭션이기 때문에 예외가 발생하면 무조건 트랜잭션 전체를 &lt;code&gt;rollback-only&lt;/code&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;a href=&quot;https://techblog.woowahan.com/2606/&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;span style=&quot;color: #9d9d9d;&quot;&gt;따라서 &lt;code&gt;Propagation.REQUIRES_NEW&lt;/code&gt; 해야 새로운 트랜잭션이 만들어지고 새로운 자식 트랜잭션에만 rollback 마킹이 되어 부모 트랜잭션에는 영향을 주지 않게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. @TransactionalEventListener + REQUIRES_NEW&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 이를 해결하기 위해 @TransactionalEventListener를 사용할 수 있는 것 같았다. 이 어노테이션은 이벤트를 리스닝하는데, 이벤트를 발행한 트랜잭션이 &lt;b&gt;종료 (커밋/롤백) 된 후에 이벤트를 처리&lt;/b&gt;한다. 따라서 위 상황에 적용하면 &lt;code&gt;일정 서비스가 커밋&lt;/code&gt;된 이후에 &lt;code&gt;알림 트랜잭션을 시작&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;style3&quot;&gt;&amp;nbsp;24. 01. 03. 수정&lt;br /&gt;이 부분도 기존에는 롤백을 막기 위해 @TransactionalEventListener를 사용했으나 위 수정 내용을 통해 다른 방법으로 해결할 수 있음을 알게 되었다. 다만, 그러한 방법(try-catch &amp;amp; REQUIRES_NEW) 으로 트랜잭션을 분리하면 알림을 보내는 모든 코드에 try-catch 문을 넣어주어야 하므로 여전히 Event를 사용하고 이를 통해 코드의 결합도를 낮추는 것이 의미가 있다고 생각했다.&lt;/blockquote&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;img src=&quot;https://blog.kakaocdn.net/dn/t6L7r/btsB3uJzZtb/0hOs3bRwl24ju8zn1QJhv1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 알림 서비스에 TransactionalEventListener 어노테이션을 달아준다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@TransactionalEventListener
@Transactional
public void send(NotificationEvent event) {
    Member targetMember = findMemberById(event.getTargetMemberId());

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 일정 서비스에서 이벤트를 publish 해준다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FWdyE/btsBYehj9cw/muIfJGXpOPCK7kaqv9ow61/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다시 테스트를 해봤다. 그 결과 의도한대로 일정 트랜잭션이 커밋된 후에 이벤트가 리스닝되어 알림 서비스에서의 예외 발생이 일정 서비스의 커밋/롤백에 영향을 주지 않는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRKLUy/btsB1xgfyJA/1H749vA710SEOiWQBR1bK0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅을 해봐도 (일정 초대 요청을 했으므로) 제대로 일정 멤버가 추가된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l9GNR/btsBVyOccZp/ISlHUjmxQTjNJcdlZStYSK/img.png&quot; alt=&quot;&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&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPiekH/btsB1JOfdLM/WbjA9EuPKoOWPr84zk71gk/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 Transactional 의 propagation 옵션 때문인데, 새로운 트랜잭션을 생성하지 않는 DEFAULT 옵션인 &lt;code&gt;PROPAGATION_REQUIRED&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;당황스럽지만 이때 앞서 시도했던 해결책인 &lt;code&gt;REQUIRES_NEW&lt;/code&gt; 로 propagation 옵션을 바꿔주면&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/miQXl/btsB3vuW7HU/mzsoIWQVztqdWO4bv9ioSK/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 트랜잭션이 생성되면서 테스트가 통과하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpjfuV/btsB1Gc1TUt/tb2T8XDKTqki2V5wsJZF6k/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 비동기 처리&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;아래 로그를 보면 일정, 알림, 푸시 서비스가 모두 동일한 쓰레드에서 실행되는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNUt4H/btsB0dhR07T/e1NHlrgyo3hLFJ9CdyZKo0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 알림 서비스가 비동기로 실행되도록 해주었다. 알림 서비스에 @Async를 달고 적당한 설정 클래스에 @EnableAsync 어노테이션을 달아주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;24. 01. 03. 수정&lt;br /&gt;추가로, @Async를 통해 비동기를 사용하면 사실 트랜잭션 분리는 신경쓰지 않아도 된다. 말 그대로 별도의 쓰레드에서 실행되기 때문에 비동기 쓰레드에서의 작업이 기존 쓰레드에서의 작업에 영향을 주지 않기 때문이다. 트랜잭션 분리만을 위해서 비동기를 사용하는 것은 적절하지 않겠지만 이 포스팅에서는 비동기 자체가 필요하여 트랜잭션 분리 이후에 비동기도 적용해주었다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;알림 서비스&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byiH3u/btsB1OhzS8I/AO0KhWoYaBSFkfJAyQaPeK/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설정 클래스&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uvSeE/btsBY04kCgV/l5OOg940rEBACGTDh7W6t1/img.png&quot; alt=&quot;&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;img src=&quot;https://blog.kakaocdn.net/dn/CrEnC/btsBY7WwfRa/IkjoV3v7O4z8kQXkq6rPiK/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 비동기로 알림/푸시를 보낼 때와 동기로 보낼 때 실제 API 응답 시간에 성능 차이가 있는지 정말 간단하게만 측정을 해보았다. 회원 A가 회원 B를 일정 A에 초대하고 탈퇴시키는 요청(두 요청 모두 알림 발송 대상)을 각각 100번 반복하게 하고 응답 시간을 평균내 보았다.&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;img src=&quot;https://blog.kakaocdn.net/dn/dnm15M/btsB1I9Elv4/mmAm2dVSOp1v0q6REVtH1k/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 비동기로 알림을 보내는 경우이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KzUao/btsB1virr06/I8rMiAfBk2NmgDdLNbkzC0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 &lt;b&gt;API 응답 시간이 무려 평균 210.12ms 에서 6.14ms로 약 34배 개선&lt;/b&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;이렇게 부수적인 로직에서 발생한 예외가 주요 로직에 영향을 주지 않도록 트랜잭션을 분리하고, 클라이언트가 보다 빠르게 응답을 받아볼 수 있도록 코드 개선을 해보았다. 개선된 시간을 보니 실제 서비스되고 있었다면 유저가 별거 아닌 요청에 엄청 쓸데없이 오래 기다리고 있었겠구나 하는 생각이 들었다. 외부 API 호출은 꼭 비동기로 분리하자!&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kth990303.tistory.com/387&quot;&gt;[Spring] REQUIRES_NEW 옵션만으론 자식이 롤백될 때 부모도 롤백된다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devvkkid.tistory.com/269&quot;&gt;Event 처리는 비동기가 아니다?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jeong-pro.tistory.com/238&quot;&gt;Spring ApplicationEvent 비동기로 처리될 것만 같지?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dkswnkk.tistory.com/704&quot;&gt;스프링 이벤트 발행과 구독으로 트랜잭션 분리하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sabarada.tistory.com/188&quot;&gt;[Spring] Spring의 Event를 어떻게 사용하는지에 대해서 알아봅시다. -&lt;/a&gt; &lt;a href=&quot;https://github.com/TransactionalEventListener&quot;&gt;@TransactionalEventListener&lt;/a&gt;&lt;a href=&quot;https://sabarada.tistory.com/188&quot;&gt;에 대해서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eocoding.tistory.com/94&quot;&gt;@Transactional 분리가 안되는 이유 / 실험을 통해 트랜잭션 전파 유형과 Spring AOP 이해&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://deveric.tistory.com/86&quot;&gt;[Spring] 트랜잭션의 전파 설정별 동작&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발 공부/Spring</category>
      <category>async</category>
      <category>REQUIRES_NEW</category>
      <category>TransactionalEventListener</category>
      <category>외부API</category>
      <category>트랜잭션 분리</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/143</guid>
      <comments>https://gmelon.dev/143#entry143comment</comments>
      <pubDate>Mon, 27 Nov 2023 17:53:30 +0900</pubDate>
    </item>
    <item>
      <title>[플랭고] 회원 가입 프로세스 개선하기</title>
      <link>https://gmelon.dev/142</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;여름부터 개발을 시작했던 플랭고가 이제 거의 배포 직전이라 자잘한 부분을 수정하고 있다. EC2에 백엔드 서버를 띄우고 애플에서 제공하는 테스트용 배포 플랫폼 TestFlight를 사용해 팀원들에게 앱을 배포하여 여러 기능을 테스트 하던 중 &lt;strong&gt;회원 가입 프로세스에 메일 인증 과정을 도입해야 하지 않겠냐는 피드백&lt;/strong&gt;을 받았다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황&quot;&gt;문제 상황&lt;/h2&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;img src=&quot;https://blog.kakaocdn.net/dn/Kku4i/btsBXReBGcP/juQkZobSAxXJIgc46B6ejk/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;해결-과정&quot;&gt;해결 과정&lt;/h2&gt;
&lt;h3 id=&quot;1-메일-인증-로직-도입&quot;&gt;1. 메일 인증 로직 도입&lt;/h3&gt;
&lt;p&gt;먼저 첫번째로 백엔드에 메일을 인증하는 로직을 추가했다. 앱에서 메일을 입력하고 인증을 요청하면 서버에서는 랜덤 토큰을 생성해 입력한 메일로 보내게 된다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg9fmM/btsBY2fxU16/4NQBoQLyopH0MCCqFoAUs1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그럼 이렇게 html과 css로 한땀한땀 작성한 작고 소중한 메일이 오게 된다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cahah0/btsBZGi1FKf/an2xFFabAwnbUZf8K2jfo1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그리고 유저가 서버에 회원 가입 요청 시 앱에서 위 인증 코드를 입력해야만 회원 가입이 성공하도록 검증하는 코드를 추가했다.&lt;/p&gt;
&lt;h3 id=&quot;2-토큰-자동-삭제-기능-구현&quot;&gt;2. 토큰 자동 삭제 기능 구현&lt;/h3&gt;
&lt;p&gt;위와 같이 개발을 마치고 나니 &lt;strong&gt;토큰이 얼마 동안 유효해야 할까?&lt;/strong&gt; 에 대한 고민이 들었다. 이에 대한 제한이 없다면 오늘 메일 인증을 수행하고 발급받은 토큰으로 내일이나 모레 심지어 일주일 뒤에도 가입이 가능하다는 건데 그건 문제가 있을 것 같았다. 그래서 위 방법으로 발급 받은 토큰을 일정 시간이 지나면 삭제하는 로직을 구현하기로 했다.&lt;/p&gt;
&lt;p&gt;이를 구현하기 위해 떠오른 방법은 크게 3가지 정도였는데 먼저 두 가지 방법은 1. 스프링 배치를 사용하거나 2. DB의 프로시저를 통해 주기적으로 생성된지 일정 시간이 지난 토큰을 삭제해주는 것이다. 하지만 첫번째 방법의 문제는 스프링 배치를 써본 적이 없다는 것이고 (이거 때문에 배치를 지금 공부하기는 시간이 부족하다고 생각했다) 두번째로 하자니 이메일 토큰을 저장하려고 RDB까지 쓰는게 맞나? 싶었다.&lt;/p&gt;
&lt;p&gt;그래서 생각한 세번째 방법은 &lt;strong&gt;Redis를 사용하는 것&lt;/strong&gt;이다. Redis는 key:value 형태로 데이터를 저장하므로 중요한 정보가 아닌 일회성 데이터인 토큰을 간단하게 저장하기에 좋아보였고 일정 시간이 지나면 자동으로 값을 지워주는 기능을 내장하고 있기 때문에 편리하게 사용할 수 있을 것 같았다. 그래서 아래와 같이 Redis에서 사용할 엔티티를 작성하고 &lt;code&gt;TimeToLive&lt;/code&gt; 를 30분으로 설정해주어 유저가 메일을 인증한 뒤 30분이 지나면 더 이상 해당 토큰을 사용할 수 없도록 처리해주었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Getter
@RedisHash(value = &quot;emailToken&quot;, timeToLive = 30 * 60)
public class EmailToken {
    @Id
    private String email;

    private String tokenValue;

    private boolean authenticated;

    @Builder
    public EmailToken(String email, String tokenValue) {
        this.email = email;
        this.tokenValue = tokenValue;
        this.authenticated = false;
    }

...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래와 같이 인증 코드 저장과 TTL이 잘 적용된 것을 확인할 수 있다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/47tzJ/btsBVC94UR1/O3tSqhPYmtXc5IHLWPD16k/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;3-토큰-검증-api&quot;&gt;3. 토큰 검증 API&lt;/h3&gt;
&lt;p&gt;사실 이대로만 해도 서버 입장에서는 메일이 유효한 유저가 가입되는 것을 보장할 수 있지만 처음에 말했던 문제점처럼 유저 입장에서는 회원 가입의 마지막 단계에 가야만 인증 토큰 유효 여부에 대한 응답을 받을 수 있으므로 회원 가입 첫 단계에서 토큰을 검증할 수 있는 API를 만들어야겠다고 생각했다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5iiOe/btsBQpDO6Ew/e3cYWSiKKaH9Tq0kSZP8p0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;위와 같이 GET 메서드로 메일 토큰에 대해 검증할 수 있도록 해주었다. 서비스에서 Redis에 저장된 토큰과 입력 값을 비교해 StatusResponseDto 라는 상태 값에 대한 DTO를 반환하도록 했고, 이 DTO가 가진 상태 코드에 따라 200 이나 400 코드를 클라이언트로 내려줄 수 있도록 컨트롤러에서는 ResponseEntity를 반환하도록 작성했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekuIKN/btsB0f6HC1J/1eKWcmkGyD8jAdMcohwB10/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;서비스에서 토큰을 검증하고 검증 결과에 따라 &lt;code&gt;ok()&lt;/code&gt; 나 &lt;code&gt;error()&lt;/code&gt; 메서드를 통해 상태 DTO를 생성해 반환하게 된다.&lt;/p&gt;
&lt;p&gt;이런 식으로 상태를 내려주는게 맞는 방법인지는 모르겠다. 에러 메시지를 추가하거나 기타 보완할 부분이 있을 것같다. 다만 기존에는 토큰이 일치하지 않았을 때 예외를 던지고 기존에 사용하던 공통 예외 처리 로직이 에러 응답을 내리도록 할까도 싶었는데 이 API는 목적이 &lt;strong&gt;토큰 검증&lt;/strong&gt; 이므로 &lt;code&gt;토큰이 일치하지 않는다&lt;/code&gt; 라는게 예외 흐름이 아니라고 생각이 들었다. 따라서 공통 예외 처리 로직을 사용하면 &lt;strong&gt;정상 흐름을 제어하는데 예외를 사용하는 상황&lt;/strong&gt;이 될 것 같아서 별도의 상태 DTO를 만들어 응답으로 내려주도록 했다.&lt;/p&gt;
&lt;h2 id=&quot;결과&quot;&gt;결과&lt;/h2&gt;
&lt;p&gt;결과적으로 앱에서 유저 플로우는 아래와 같이 변경되었다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBJyQP/btsBVDulm0B/k9vCMxw5lwkhkY8INOHa0k/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;사용자 입장에서 확실히 기존 방식보다 편리하게 가입할 수 있을 것 같다. 얼른 배포까지 진행해서 실제 피드백을 받아보고 싶다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>프로젝트/[앱] 플랭고</category>
      <category>Redis</category>
      <category>Redis TimeToLive</category>
      <category>메일 인증</category>
      <category>회원가입</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/142</guid>
      <comments>https://gmelon.dev/142#entry142comment</comments>
      <pubDate>Sat, 14 Oct 2023 22:31:19 +0900</pubDate>
    </item>
    <item>
      <title>[플랭고] JPA delete() 쿼리 안 나가는 문제 해결</title>
      <link>https://gmelon.dev/141</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;h2 id=&quot; -문제-상황&quot; data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용하다가 &lt;code&gt;xxxRepository.delete(Entity entity)&lt;/code&gt; 메서드를 통해 엔티티를 삭제하고 싶었는데 아무리 해도 delete 쿼리가 발생하지 않는 문제가 있었다.&lt;/p&gt;
&lt;h4 id=&quot;엔티티-삭제-코드&quot; data-ke-size=&quot;size20&quot;&gt;엔티티 삭제 코드&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional
public void rejectOrExitSchedule(Long memberId, Long scheduleId) {
    Schedule schedule = findScheduleById(scheduleId);
    validateMemberNotOwner(memberId, schedule);

    ScheduleMember scheduleMember = findScheduleMemberByMemberIdAndScheduleId(memberId, scheduleId);
    scheduleMemberRepository.delete(scheduleMember);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;테스트-코드&quot; data-ke-size=&quot;size20&quot;&gt;테스트 코드&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/9qehj/btsv7g56rZP/lKHi1mwwet9VKLNdhp8sQk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;테스트-실행-결과&quot; data-ke-size=&quot;size20&quot;&gt;테스트 실행 결과&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzA3Zb/btsvY0C412N/4ealKHvb76PympNZEAqbkK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 말고 id를 인자로 받는 메서드를 통해 제거를 시도해도 JPA 구현체 내부에서는 다시 delete()를 사용하기 떄문에 마찬가지 결과가 나왔다.&lt;/p&gt;
&lt;h4 id=&quot;id를-인자로-받는-메서드를-통해-제거를-시도&quot; data-ke-size=&quot;size20&quot;&gt;id를 인자로 받는 메서드를 통해 제거를 시도&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVeT0M/btsv7z5BXXF/UU973EjUK5j7iZbi20bV4k/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 다음과 같이 JPQL로 직접 delete 쿼리를 날리는 메서드를 작성하고 호출하는 방법으로 임시로 해결하긴 했지만, 왜 이러한 현상이 발생하는지 이해가 되질 않아 원인을 찾아보았다.&lt;/p&gt;
&lt;h4 id=&quot;레포지토리-jpql-코드&quot; data-ke-size=&quot;size20&quot;&gt;레포지토리 JPQL 코드&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rtgg4/btsv5pWAbS6/r3poFuyMFscyhIDIyeP8ik/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;delete-메서드-호출&quot; data-ke-size=&quot;size20&quot;&gt;delete 메서드 호출&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;...
scheduleMemberRepository.deleteByMemberIdAndScheduleId(memberId, scheduleId);
...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;실행-결과&quot; data-ke-size=&quot;size20&quot;&gt;실행 결과&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCjHpr/btsv7xUeIug/srHJYlJRxY7VIGAC0Br6cK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot; -원인&quot; 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;code&gt;ScheduleMember&lt;/code&gt; 엔티티이고, 이 엔티티는 일정 엔티티인 &lt;code&gt;Schedule&lt;/code&gt; 과 양방향 연관관계를 갖는다.&lt;/p&gt;
&lt;h4 id=&quot;엔티티-연관관계&quot; data-ke-size=&quot;size20&quot;&gt;엔티티 연관관계&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSZLRH/btsv0u4WlAX/REIek9crQJ1YJhfK1w9xIk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 앞서 살펴본 문제의 코드에서 검증 로직 수행을 위해 Schedule(ScheduleMember를 fetch join 함)을 먼저 조회하게 되면 영속성 컨텍스트에 Schedule 엔티티가 올라가게 되고, 이후 ScheduleMember를 조회하면 이미 영속성 컨텍스트에 동일 id의 ScheduleMember가 존재하기 때문에 select 쿼리 없이 캐시에서 엔티티가 조회된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/TQHED/btswbDlrDiK/47jdYfIenW9QmskahofrEk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅을 통해 참조값을 확인해보면 Schedule의 &lt;code&gt;List&amp;lt;ScheduleMember&amp;gt;&lt;/code&gt; 에 포함된 ScheduleMember와 이후에 조회한 ScheduleMember가 동일한 인스턴스임을 확인할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/TIDnL/btsv0pif45G/6F7KuScQFfqTKx98u0qh91/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot; -해결-방안&quot; data-ke-size=&quot;size26&quot;&gt;  해결 방안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 문제를 해결하기 위해서는 delete() 메서드 호출 전에 다른 엔티티와의 연관관계를 끊어주면 된다. 이 경우 Schedule의 리스트에서 ScheduleMember를 제거해주면 정상적으로 delete 쿼리가 나가는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h4 id=&quot;schedule&quot; data-ke-size=&quot;size20&quot;&gt;Schedule&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 Schedule에 scheduleMembers 리스트에서 주어진 ScheduleMember을 제거하는 메서드를 만들고,&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public void deleteScheduleMember(ScheduleMember scheduleMember) {
    scheduleMembers.remove(scheduleMember);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 delete 쿼리를 날리기 전에 호출해주면,&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/24gjM/btsv0visUJg/ivTPkAIVHkAbWZypN7GgSK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;repository.delete()&lt;/code&gt; 메서드를 통해서도 다음과 같이 제대로 delete 쿼리가 나가는 것을 확인할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6HkcC/btsv0pbqOYo/4my8rIMS71fnkByHPo5JAk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 이런식으로 Schedule에서 ScheduleMember를 제거하도록 구현할거면 orphanRemoval 기능을 사용해서 별도로 &lt;code&gt;ScheduleMemberRepository의 delete 메서드&lt;/code&gt;를 호출하지 않고 Schedule의 &lt;code&gt;deleteScheduleMember()&lt;/code&gt; 만 호출해도 delete 쿼리가 나가도록 구성해도 괜찮을 것 같다는 생각이 들었다.&lt;/p&gt;
&lt;h2 id=&quot;참고-자료&quot; data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/22688402/delete-not-working-with-jparepository&quot;&gt;https://stackoverflow.com/questions/22688402/delete-not-working-with-jparepository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/article&gt;</description>
      <category>개발 공부/Spring</category>
      <category>delete 쿼리</category>
      <category>JPA</category>
      <category>JPA delete()</category>
      <category>연관관계</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/141</guid>
      <comments>https://gmelon.dev/141#entry141comment</comments>
      <pubDate>Thu, 28 Sep 2023 12:48:39 +0900</pubDate>
    </item>
    <item>
      <title>[플랭고] 반정규화 + Lock vs 정규화 + 서브쿼리 - 누가 더 빠를까</title>
      <link>https://gmelon.dev/140</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;h2 id=&quot;배경&quot; 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;b&gt;'일정 목록 조회'&lt;/b&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;현재 Schedule 테이블에 일정 관련 정보가 있고, ScheduleMember 테이블에 일정에 참여하는 회원에 대한 정보가 담겨있는데 앱에서 일정 목록 조회 시 다음과 같이 일정에 참여하는 회원 수를 표시해주기 위해서 &lt;b&gt;목록 조회 시 Schedule과 ScheduleMember를 같이 조회해야 되는 상황&lt;/b&gt;이 생겼다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl5Agt/btsBKfMIO0T/YiXVPiqEwqm81d8FKgVF71/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 구현하기 위해 여러 방법을 사용할 수 있을 것 같았지만 각 방법 별 성능 차이를 확인하고 싶어서 jmeter를 사용해 직접 성능을 측정해보았다.&lt;/p&gt;
&lt;h2 id=&quot;테스트-준비&quot; data-ke-size=&quot;size26&quot;&gt;테스트 준비&lt;/h2&gt;
&lt;h3 id=&quot;테스트에-사용할-api-들&quot; data-ke-size=&quot;size23&quot;&gt;테스트에 사용할 api 들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 테스트는 일정 목록을 조회하는 api와 '함께할 친구'를 변경하는 api를 호출하여 각각의 response time을 측정하는 방법으로 진행했다. 일반적으로 일정 참여 회원의 변경 요청보다는 목록 조회가 많을 것이므로 일정 목록을 50번 조회할 때 회원 변경 요청이 2번 이뤄진다고 가정하고 테스트를 진행했다.&lt;/p&gt;
&lt;h4 id=&quot;1-일정-목록-조회-api&quot; data-ke-size=&quot;size20&quot;&gt;1. 일정 목록 조회 api&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(params = &quot;date&quot;)
public List&amp;lt;ScheduleListResponseDto&amp;gt; findAllByDate(
        @LoginMember Long memberId,
        @DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;) @RequestParam(&quot;date&quot;) LocalDate requestDate) {
    return scheduleService.findAllByDate(memberId, requestDate);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 호출해 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /api/auth/schedules?date=yyyy-MM-dd&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-함께할-친구-변경-api&quot; data-ke-size=&quot;size20&quot;&gt;2. 함께할 친구 변경 api&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@PostMapping
public void invite(@LoginMember Long memberId,
                   @PathVariable Long scheduleId,
                   @RequestBody @Valid ScheduleMemberAddRequestDto requestDto) {
    scheduleMemberService.invite(memberId, scheduleId, requestDto);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 호출한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;POST /api/schedules/{scheduleId}/members

{&quot;memberId&quot; : &quot;{memberId}&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;테스트-데이터-입력--jmeter-설정&quot; data-ke-size=&quot;size23&quot;&gt;테스트 데이터 입력 &amp;amp; jmeter 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 진행하기 위해 먼저 100명의 회원을 생성하고 각 회원이 공유하지 않는 일정 5개와 공유하는 일정 2개를 갖도록 @PostConstruct를 사용해 데이터를 입력해주었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@PostConstruct
public void dataInit() {
    // 회원 100명 가입
    // user1, user2, ..., user100
    IntStream.rangeClosed(1, 100)
            .mapToObj(number -&amp;gt; SignupRequestDto.builder()
                    .nickname(&quot;user&quot; + number)
                    .password(&quot;1111&quot;)
                    .email(number + &quot;@gmelon.dev&quot;)
                    .build())
            .forEachOrdered(authService::signup);

    List&amp;lt;Member&amp;gt; members = memberRepository.findAll();

    // 회원 별 자신이 소유하는 일정 5개 생성
    int singleScheduleCountPerMember = 5;
    for (Member member : members) {
        for (int i = 0; i &amp;lt; singleScheduleCountPerMember; i++) {
            Schedule schedule = Schedule.builder()
                    .title(&quot;일정&quot;)
                    .content(&quot;내용&quot;)
                    .date(LocalDate.now())
                    .startTime(LocalTime.now())
                    .build();
            schedule.setSingleOwnerScheduleMember(member);
            scheduleRepository.save(schedule);
        }
    }

    // 회원 별 랜덤한 다른 회원 2명과 공유하는 일정 2개 생성
    int shareScheduleCountPerMember = 2;
    ThreadLocalRandom random = ThreadLocalRandom.current();
    for (Member member : members) {
        for (int i = 0; i &amp;lt; shareScheduleCountPerMember; i++) {
            Member firstMember = members.get(random.nextInt(100));
            while (firstMember.getId().equals(member.getId())) {
                firstMember = members.get(random.nextInt(100));
            }
            Member secondMember = members.get(random.nextInt(100));
            while (secondMember.getId().equals(member.getId()) || secondMember.getId().equals(firstMember.getId())) {
                secondMember = members.get(random.nextInt(100));
            }

            ScheduleCreateRequestDto requestDto = ScheduleCreateRequestDto.builder()
                    .title(&quot;일정&quot;)
                    .content(&quot;내용&quot;)
                    .date(LocalDate.now())
                    .startTime(LocalTime.now())
                    .participantIds(List.of(firstMember.getId(), secondMember.getId()))
                    .build();
            scheduleService.create(member.getId(), requestDto);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 테스트를 수행하기 위해 다음과 같이 jmeter Test Plan을 작성했다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/zPVXE/btsBDPI3X5U/R6tSfIhtL2yYaHbbANuZq0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 설명하면, csv 파일에 작성된 회원 닉네임과 비밀번호 를 읽어 로그인을 수행하고 (HTTP 쿠키 매니저를 통해 사용자별 쿠키를 유지) 각 회원 별로 스케쥴 목록 조회를 50번 수행하고 함께할 친구 추가를 2번 수행하는 과정을 총 2회 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림으로 보면 아래와 같은 과정으로 테스트를 진행한다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/UjDas/btsBFf8GPka/zhNk0DJK8sC92Q5PJhe6g1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;테스트-진행&quot; data-ke-size=&quot;size26&quot;&gt;테스트 진행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 마치고 실제 성능 테스트를 진행했다.&lt;/p&gt;
&lt;h3 id=&quot;1-정규화--n--1-쿼리&quot; data-ke-size=&quot;size23&quot;&gt;1. 정규화 &amp;amp; N + 1 쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 이야기한 '일정 목록 조회 시 일정에 참여하는 회원 수 함께 조회' 라는 문제를 해결하기 위해 먼저 기존 테이블대로 &lt;b&gt;정규화를 유지&lt;/b&gt;하고 매번 일정 목록 조회 시 마다 일정(schedule)에 해당하는 일정 회원(scheduleMember)이 몇 개가 존재하는지 &lt;b&gt;애플리케이션에서 추가 쿼리를 날리는 방법&lt;/b&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;h4 id=&quot;schedulequeryrepository&quot; data-ke-size=&quot;size20&quot;&gt;ScheduleQueryRepository&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Query(&quot;select new dev.gmelon.plango.domain.schedule.query.dto.ScheduleListQueryDto(&quot; +
            &quot;s.id, s.title, s.content, s.date, s.startTime, s.endTime, sm.owner, sm.accepted, &quot; +
            &quot;s.latitude, s.longitude, s.roadAddress, s.placeName, s.done) &quot; +
        &quot;from Schedule s join s.scheduleMembers sm &quot; +
        &quot;where sm.member.id = :memberId &quot; +
        &quot;and s.date = :date &quot; +
        &quot;order by case when s.startTime is null then 0 else 1 end, &quot; +
        &quot;case when s.startTime is null then s.modifiedDate else s.startTime end asc, &quot; +
        &quot;s.endTime asc&quot;)
List&amp;lt;ScheduleListQueryDto&amp;gt; findAllByMemberIdAndDate(@Param(&quot;memberId&quot;) Long memberId, @Param(&quot;date&quot;) LocalDate date);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;scheduleservice&quot; data-ke-size=&quot;size20&quot;&gt;ScheduleService&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository에서 조회한 &lt;code&gt;ScheduleListQueryDto&lt;/code&gt; 를 api의 응답으로 나갈 &lt;code&gt;ScheduleListResponseDto&lt;/code&gt; 로 변환할 때 회원 수를 조회하기 위해 schduleMemberRepository의 메서드를 매번 호출하게 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public List&amp;lt;ScheduleListResponseDto&amp;gt; findAllByDate(Long memberId, LocalDate requestDate) {
    List&amp;lt;ScheduleListQueryDto&amp;gt; schedules = scheduleQueryRepository.findAllByMemberIdAndDate(memberId, requestDate);
    return schedules.stream()
            .map(schedule -&amp;gt; ScheduleListResponseDto.from(schedule, scheduleMemberRepository.countByScheduleId(schedule.getId())))
            .collect(toList());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 계층에서 조회한 일정별로 다시 쿼리를 날려야 하기 때문에 (&lt;code&gt;scheduleMemberRepository.countByScheduleId()&lt;/code&gt;) N + 1 쿼리 문제가 발생하게 된다. 성능을 측정해보면 다음과 같이 나온다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/I7FdP/btsBDPbefkp/PdiiDyeATgazpP2QVoz4v1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트 중 발생한 에러는 1 - 1000 사이의 id 중 랜덤하게 택한 id를 갖는 회원을 함께할 친구로 추가할 때 이미 함께할 친구로 지정되어 있어서 발생하는 오류로 우선 무시하고, 스케쥴 목록 조회와 함께할 친구 추가에 소요된 평균 시간을 확인했다. 각각 2561ms, 1507ms로 측정되었다. 즉, 평균적으로 목록을 조회하는데 2.5초가 걸렸다는 의미이다. 빠르다고는 할 수 없는 시간이지만 다른 결과가 없어 비교하기가 어려워 우선 나머지 항목들도 마저 측정해보았다.&lt;/p&gt;
&lt;h3 id=&quot;2-정규화--select-절-서브-쿼리&quot; data-ke-size=&quot;size23&quot;&gt;2. 정규화 &amp;amp; select 절 서브 쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 1번 경우와 마찬가지로 &lt;b&gt;정규화를 유지&lt;/b&gt;하지만, 애플리케이션 레벨에서 쿼리를 추가로 날리는 것이 아니라 &lt;b&gt;db 레벨에서 서브 쿼리를 날려 원하는 값을 조회&lt;/b&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;h4 id=&quot;schedulequeryrepository-1&quot; data-ke-size=&quot;size20&quot;&gt;ScheduleQueryRepository&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select 절에서 서브 쿼리를 사용해 현재 스케쥴 id를 schedule_id로 갖는 ScheduleMember의 row 개수를 조회한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;@Query(&quot;select new dev.gmelon.plango.domain.schedule.query.dto.ScheduleListQueryDto(&quot; +
            &quot;s.id, s.title, s.content, s.date, s.startTime, s.endTime, (select count(*) from ScheduleMember sm where sm.schedule.id = s.id), &quot; +
            &quot;sm.owner, sm.accepted, s.latitude, s.longitude, s.roadAddress, s.placeName, s.done) &quot; +
        &quot;from Schedule s join s.scheduleMembers sm &quot; +
        &quot;where sm.member.id = :memberId &quot; +
        &quot;and s.date = :date &quot; +
        &quot;order by case when s.startTime is null then 0 else 1 end, &quot; +
        &quot;case when s.startTime is null then s.modifiedDate else s.startTime end asc, &quot; +
        &quot;s.endTime asc&quot;)
List&amp;lt;ScheduleListQueryDto&amp;gt; findAllByMemberIdAndDate(@Param(&quot;memberId&quot;) Long memberId, @Param(&quot;date&quot;) LocalDate date);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;scheduleservice-1&quot; data-ke-size=&quot;size20&quot;&gt;ScheduleService&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, ScheduleService는 다음과 같이 &lt;b&gt;추가로 쿼리를 날리지 않는 방식으로 개선&lt;/b&gt;될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public List&amp;lt;ScheduleListResponseDto&amp;gt; findAllByDate(Long memberId, LocalDate requestDate) {
    List&amp;lt;ScheduleListQueryDto&amp;gt; schedules = scheduleQueryRepository.findAllByMemberIdAndDate(memberId, requestDate);
    return schedules.stream()
            .map(ScheduleListResponseDto::from)
            .collect(toList());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에 성능을 측정해보면 다음과 같은 결과가 나오게 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/LEITD/btsBFMykaVX/aBlokow47bs2b4jyK9u081/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 좀 깜짝놀랐다. 이전 테스트와 비교해 너무 빨리 끝나서 처음엔 테스트가 제대로 안 된건줄알고 다시 돌려보기도 했다. '함께할 친구 추가' api의 응답 시간은 1507ms에서 258ms로 5.84배 가량 빨라졌고 특히 '스케쥴 목록 조회' api의 응답 시간은 2561ms 에서 27ms로 94배 가량 빨라졌다. N+1 쿼리 문제.. 많이 들어보기는 했지만 이렇게 직접 수치로 확인하니 더 와닿았다. 그냥 N+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;h3 id=&quot;3-반정규화--비관적-lock&quot; data-ke-size=&quot;size23&quot;&gt;3. 반정규화 &amp;amp; 비관적 Lock&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 반정규화를 하기 위해 다음과 같이 Schedule 테이블에 &lt;code&gt;memberCount&lt;/code&gt; 라는 필드를 두고 &lt;b&gt;'함께할 친구'를 추가하고 삭제할 때마다 이 값을 변경&lt;/b&gt;하도록 했다.&lt;/p&gt;
&lt;h4 id=&quot;schedule&quot; data-ke-size=&quot;size20&quot;&gt;Schedule&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = PROTECTED)
@Entity
public class Schedule extends BaseTimeEntity {

    /*생략*/

    @Column(nullable = false)
    private int scheduleMemberCount;

    public void increaseScheduleMemberCount() {
        this.scheduleMemberCount++;
    }

    public void decreaseScheduleMemberCount() {
        this.scheduleMemberCount--;
    }

    /*생략*/

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;schedulememberservice&quot; data-ke-size=&quot;size20&quot;&gt;ScheduleMemberService&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 ScheduleMember 테이블에 쿼리를 날리는 것으로 '함께할 친구'의 수를 알 수 있었지만, 반정규화를 하면 추가로 쿼리를 날리지 않고 Schedule의 필드를 그대로 반환하게 되기 때문에 &lt;b&gt;ScheduleMember 테이블의 값이 변경될 때 마다 Schedule 테이블의 값도 함께 변경&lt;/b&gt;해주어야 한다. 따라서 다음과 같이 ScheduleMemberService에서 연관된 Schedule의 &lt;code&gt;scheduleMemberCount&lt;/code&gt;를 변경해주도록 한다. 추가, 수정, 삭제 등 모든 곳에 반영되어하며 예시로는 추가 코드만 가져왔다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional
public void invite(Long scheduleOwnerMemberId, Long scheduleId, ScheduleMemberAddRequestDto requestDto) {
    Schedule schedule = findScheduleById(scheduleId);
    validateOwner(schedule, scheduleOwnerMemberId);

    Long newMemberId = requestDto.getMemberId();
    validateMemberNotExists(schedule, newMemberId);

    saveNewScheduleMember(newMemberId, schedule);
    // 연관된 Schedule의 scheduleMemberCount를 증가시킨다
    schedule.increaseScheduleMemberCount();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이렇게 하니, 동시에 여러 회원이 '함께할 친구'를 변경할 때 동시성 문제로 Schedule이 갖는 memberCount와 ScheduleMember 테이블의 개수가 일치하지 않는 문제가 종종 발생하는 것을 확인했고 이를 방지하기 위해 DB 레벨에 Lock을 걸어주었다.&lt;/p&gt;
&lt;h4 id=&quot;schedulerepository&quot; data-ke-size=&quot;size20&quot;&gt;ScheduleRepository&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Query(&quot;select s from Schedule s &quot; +
        &quot;join fetch s.scheduleMembers sm &quot; +
        &quot;join fetch sm.member &quot; +
        &quot;where s.id = :id&quot;)
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({@QueryHint(name = &quot;jakarta.persistence.lock.timeout&quot;, value = &quot;10000&quot;)})
Optional&amp;lt;Schedule&amp;gt; findByIdWithScheduleMembersWithLock(@Param(&quot;id&quot;) Long scheduleId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;code&gt;@Lock&lt;/code&gt;은 &lt;code&gt;distinct&lt;/code&gt;와 같이 사용할 수 없다고 한다. 동시에 사용하려고 하면 &lt;code&gt;InvalidDataAccessResourceUsageException&lt;/code&gt; 이 발생한다. 하지만 JPQL에서 distinct 키워드를 사용하지 않으면 일대다 관계에서 다쪽 row의 개수 만큼 일쪽 엔티티가 뻥튀기(?) 되는 잘 알려진 문제가 있다. distinct를 빼고 다시 실행해보니 예외가 발생하지 않고 동작은 잘 했지만 확인해보니 반환 타입이 &lt;code&gt;List&amp;lt;Schedule&amp;gt;&lt;/code&gt; 로 되어있는 경우 역시나 ScheduleMember의 개수만큼 동일한 Schedule 객체가 리스트에 중복되어 포함되는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;h4 id=&quot;listltschedulegt-을-반환하는-schedulerepository&quot; data-ke-size=&quot;size20&quot;&gt;List&amp;lt;Schedule&amp;gt; 을 반환하는 ScheduleRepository&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/byPRft/btsBDLNwq2L/yAlLstE17fciGQwua00gaK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;schedulememberservice-1&quot; data-ke-size=&quot;size20&quot;&gt;ScheduleMemberService&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpMUY5/btsBGiwXtKH/oDnsyk3tbbtZw71sgqTcPK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;디버깅-결과&quot; data-ke-size=&quot;size20&quot;&gt;디버깅 결과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ScheduleMember의 개수만큼 Schedule이 중복된다)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/mC65n/btsBDPoMv3L/YKZ1twLvqtPOfQTIJqpbm0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이것이 문제가 되는 이유가 단일 row가 아니라 여러 row를 조회할 때 예상했던 row보다 많은 row가 조회되어 그 후의 로직이 의도한대로 동작하지 않는 것이라고 한다면, 이 경우에는 어차피 (리스트가 아닌) 하나의 Schedule만을 조회하는 것이고 위와 같이 동일한 주소의 객체가 조회되기 때문에 그 중 하나만 사용하도록 하면 괜찮은게 아닐까? 하는 생각을 했다. 테스트해보니 메서드의 반환 타입을&lt;code&gt;Optional&amp;lt;Schedule&amp;gt;&lt;/code&gt; 로 해두면 JPA는 조회되는 여러 Schedule 중 첫번째를 반환해주고 있기도 해서 우선 이렇게 두고 사용하기로 했다.&lt;/p&gt;
&lt;h4 id=&quot;schedulememberservice-2&quot; data-ke-size=&quot;size20&quot;&gt;ScheduleMemberService&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스에서도 락을 획득하는 메서드를 사용해 schedule를 조회하고 count를 변경해주었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private Schedule findScheduleByIdWithLock(Long scheduleId) {
    return scheduleRepository.findByIdWithScheduleMembersWithLock(scheduleId)
            .orElseThrow(NoSuchScheduleException::new);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 반정규화와 Lock을 사용해 문제를 해결하는 방법의 성능은 다음과 같았다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc4bG7/btsBHrGLQuk/nIgV0sTrHbL8kKSwzTSJak/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케쥴 목록 조회는 91ms, 함께할 친구 추가는 581ms 정도가 걸렸다. 정규화 + N+1 쿼리 방식보다는 훨씬 빠르지만 이 방식은 Lock을 획득하고 반환하는 과정이 존재하기 때문에 Lock이 없을 때보다는 읽기, 쓰기 시간이 더 소요된다. 때문에 정규화 + 서브쿼리 방식보다는 조금씩 더 시간이 걸리는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 경우의 결과를 표로 정리해보면 다음과 같다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKZWlo/btsBFeBWIJ6/jSlk9kZ2ZSLntZMP1ueRK0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막연하게 서브쿼리는 사용하면 안 된다고 생각해서 처음에 당연히 이 문제를 반정규화로 풀어야겠다고 생각했었는데 예상과는 다른 결과가 나와서 놀랐고 좋은 공부가 되었던 것 같다. 잘 모르지만 서브쿼리의 where 절에서 인덱스가 걸려있는 컬럼을 사용했기 때문에 성능에 큰 영향이 없었던게 아닐까 생각이 들었다. 나름 고민해서 구성하고 측정한 성능 테스트 결과가 있어서 개발 중인 프로젝트인 플랭고에서는 우선 정규화 + 서브쿼리로 이 문제를 푸는 것으로 결정 내릴 수 있었다.&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;이전부터 데이터베이스에 대한 깊이 있는 지식이 부족하다고 느껴서 며칠 전 이펙티브 자바 스터디 팀원들에게 Real MySQL 책에서 실행 계획, 인덱스 부분 정도라도 같이 읽어보자고 얘기했었는데 이 부분 잘 공부해서 서브쿼리나 옵티마이저 같은 내용을 좀 더 확실히 알아두면 좋겠다는 생각이 들었다.&lt;/p&gt;
&lt;/article&gt;</description>
      <category>프로젝트/[앱] 플랭고</category>
      <category>JMeter</category>
      <category>JPA Lock</category>
      <category>반정규화</category>
      <category>서브쿼리</category>
      <category>성능테스트</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/140</guid>
      <comments>https://gmelon.dev/140#entry140comment</comments>
      <pubDate>Sun, 17 Sep 2023 22:23:01 +0900</pubDate>
    </item>
    <item>
      <title>인프콘 2023 후기</title>
      <link>https://gmelon.dev/139</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;작년에 이어 올해도 인프콘에 다녀왔다! 워낙 치열한 경쟁률을 자랑하는 컨퍼런스라서 올해는 어렵겠지 생각했는데 작년 청년멘토링 프로그램을 통해 멘토링을 해주셨던 동욱님께서 너무나 감사하게도 초대권을 주셔셔 올해도 다녀올 수 있게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD4LGF/btssMBE0XCp/s5Va7lrFKOohZK5k5e1Sl1/img.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://sh-hyun.tistory.com/49#%EC%9D%B8%ED%94%84%EC%BD%98&quot;&gt;작년 인프콘 후기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요즘 너무 정신이 없어서 시간이 가는걸 잘 못 느끼고 있었는데 작년 8월에 갔던 인프콘을 올해 다시 왔다고 생각하니 1년이 지났다는걸 확 실감할 수 있었다.&lt;/p&gt;
&lt;p&gt;오프닝을 시작으로 인프콘이 시작되었다. 나름 인프콘 경력직(?)으로서 인프콘의 정수는 기업부스와 인프런 굿즈 뽑기임을 알고 있었기에 &lt;del&gt;(아닙니다)&lt;/del&gt; 세션을 듣기 전에 기업 부스를 돌며 굿즈를 받으러 다녔다. 마침 우리가 듣고 싶은 세션이 모두 오후에 몰려있기도 했다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjOi7K/btssMBrro3L/OXpLMkOm9lkx1lISsytko1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;기업-부스&quot;&gt;기업 부스&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3HvF3/btssHE98q8f/rHywlk5LdtNf4N7xUHgKF1/img.jpg&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
기업 부스를 돌며 설명을 듣고 인력풀 등록과 설문 조사, 퀴즈 등을 풀었다. (그리고 굿즈를 받았다 ) 농담처럼 말했지만 작년에도 그랬고 올해도 인프콘에 설치된 기업 부스들을 보고 있으면 뭔가 나도 저런 회사에 들어가서 개발하고 싶다는 엄청난 동기부여를 받게 된다. 부스에 나와계시는 각 회사의 개발자분들, 심지어 CTO분들을 보며 되게 많은 생각을 했던 것 같다. 기업 부스의 개발자분들 말고도 2000명 가까이되는 참가자 분들이 모두 개발자분들이기에 거기서 오는 열기나 소속감같은 것도 되게 좋았다.&lt;/p&gt;
&lt;p&gt;개인적으로는 저 이번에 새로나왔다는 인프런 캐릭터가 너무 귀여웠는데 캐릭터가 그려진 유리컵을 뽑아서 너무 기분이 좋았다 ㅋㅋ&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qd7OS/btssDNNugx2/uEKT2llAiWL5b6klQyYNo0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;세션&quot;&gt;세션&lt;/h2&gt;
&lt;h3 id=&quot;어느날-고민-많은-주니어-개발자가-찾아왔다-2탄&quot;&gt;어느날 고민 많은 주니어 개발자가 찾아왔다 2탄&lt;/h3&gt;
&lt;p&gt;세션은 총 3개를 들었다. 가장 먼저 영한님의 '어느날 고민 많은 주니어 개발자가 찾아왔다 2탄' 이다. 사실 올해는 취업 후 주니어가 성장할 때 도움이 될 만한 내용을 준비하셨다고 해서 그냥 가볍게 들어야겠다~ 하고 갔는데 생각보다 너무 지금 나에게 도움이 되는 이야기들을 들을 수 있어서 좋았다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/AoQCj/btssGwYYT9G/cbr5ovPPiKICKeE8doWpH1/img.jpg&quot; alt=&quot;&quot; /&gt;&lt;br /&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;li&gt;이른 최적화는 유지보수 비용을 증가시킴&lt;/li&gt;
&lt;li&gt;고민은 하되 최대한 단순하게 시작하고 개선하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;라고 적혀있었다. 지금 프로젝트를 개발하면서 드는 고민들과 관련된 내용이라서 더 와닿았던 내용들이었던 것 같다.&lt;/p&gt;
&lt;p&gt;이밖에도 개발자가 가져야 할 마음가짐이나 어떻게 공부하고 일해야 하는지에 대해 많은 이야기들을 해주셨고, 좋은 생각거리가 되었다. 세션을 들으면서 계속 이런 저런 생각을 하며 들었던 기억이 난다. 올해 인프콘 영상이 올라오면 다시 보면서 필요한 내용들을 다시 정리하면 좋을 것 같다.&lt;br /&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/u0Qtu/btssDKiWQC3/MuzBZzhpQ0txqTkGDtLUI1/img.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;스프링과-함께-더-나은-개발자가-되기&quot;&gt;스프링과 함께 더 나은 개발자가 되기&lt;/h3&gt;
&lt;p&gt;다음으로는 토비님의 세션이었다. 스프링 + 자바를 공부하고 있는 백엔드 개발자로서 안 들을 수 없는 세션이었다. 특히 최근에 인프런의 토비님 스프링 부트 강의를 다 듣게 되어서 더 뵙고싶기도 했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csi8Cq/btssDKpFDGd/iZq12oqg7dz05Y1m2be301/img.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;세션 내용은 전체적으로는 스프링을 통해 어떻게 자바나 다른 기술들을 학습할 수 있는지에 대한 내용이었다. 전반부에 이와 관련하여 토비님께서 직접 겪으셨던 내용을 소개해주셨는데 이 부분이 정말 정말 좋았다. 사실 저때 점심먹고 너무 졸렸는데 그게 너무 억울했다 ㅋㅋ&lt;/p&gt;
&lt;p&gt;간략하게 이야기하면 토비님께서 처음에 스프링으로 개발을 할 때 스프링에서 이렇게 하라고 해서 하긴 하지만 어떻게 동작하는지 모르고 그냥 따라했었다, 그래서 프로젝트를 성공적으로 마친 후 스프링이 제공하는 기능들이 어떻게 만들어지는건지를 공부하며 스프링 -&amp;gt; DI -&amp;gt; IoC -&amp;gt; 디자인패턴 -&amp;gt; 스프링 이런식으로 이 기술이 여기에 쓰이는거구나, 스프링의 이 기능이 바로 이거였구나 하는 등의 깨달음을 얻고 기술에 대한 더 깊은 이해를 하셨다는 내용이다.&lt;/p&gt;
&lt;p&gt;최근 이펙티브 자바 스터디를 하면서 그동안 모른척했던 자바의 API나 스프링 코드를 점차 직접 보고 분석하는 경우가 많아졌는데 이때 내가 느꼈던 감정이랑 비슷한 것 같아서 신기했다.&lt;/p&gt;
&lt;p&gt;세션 후반부에는 이렇게 학습한 내용을 어떻게 정리해야 하는지에 대한 내용도 간략히 말씀해주시는데 이 부분도 많은 도움이 되었다.&lt;/p&gt;
&lt;h3 id=&quot;인프런에서는-수천-개의-테스트-코드를-이렇게-다루고-있어요&quot;&gt;인프런에서는 수천 개의 테스트 코드를 이렇게 다루고 있어요&lt;/h3&gt;
&lt;p&gt;마지막으로 테스트 코드와 관련된 세션을 들었다. 지금 개발 중인 프로젝트에 테스트 코드를 작성하고 있는데 테스트 코드의 유지 비용이 너무 커서 내가 테스트 코드를 잘 작성하고 있는건지에 대한 고민들이 많았다. 그래서 인프런 개발자분들은 테스트 코드에 대해 어떤 생각을 가지고 계시는지 궁금해서 세션을 들어보고 싶었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC7gug/btssBo1zx82/8KrmdCCSh5z36b5Zd6vaSK/img.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;사실 세션 내용이 정확히 내가 기대한 방향은 아니었지만 그래도 인프런에서는 테스트 코드를 어떻게 작성하는지 보는 것만으로도 되게 많은 도움이 되었다. 프로젝트를 하면서 컨트롤러와 서비스 테스트가 각각 어디까지 검증할 책임을 가져야 하는지 등에 대해 고민이 되게 많았었는데 테스트 코드를 통합, 단위, E2E로 분리해서 설명해주셔서 계속 제자리걸음이던 고민이 조금은 진행될 수 있었다.&lt;/p&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;p&gt;작년에 이어 올해도 너무나 좋은 경험이었다. 작년과 비교해 올해의 나는 얼마나 성장했는지 고민하고 성찰하는 계기가 되었다. 내가 목표로 하는 것을 이룬 사람들을 직접 만나고 대화하는 경험은 힘들어도 포기하지 않고 계속할 수 있는 동기부여가 되어주는 것 같다. 내년 인프콘 때는 취업에 성공해서 또 다른 마음가짐으로 인프콘 세션을 들을 수 있게 지금 더 열심히 노력해야겠다는 다짐을 했다.&lt;/p&gt;
&lt;p&gt;듣고 싶었는데 시간과 체력 이슈로 듣지 못한 세션들을 다시보기로 보는 것도 기대가 된다! 다른 세션들을 통해서도 많은 것을 얻어갈 수 있었으면 좋겠다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>기록/후기, 회고</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/139</guid>
      <comments>https://gmelon.dev/139#entry139comment</comments>
      <pubDate>Thu, 31 Aug 2023 22:54:06 +0900</pubDate>
    </item>
    <item>
      <title>[플랭고] JPQL fetch join + where절 사용 방법과 조건</title>
      <link>https://gmelon.dev/138</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 개발 중에 fetch join 과 where 절을 함께 사용하고 싶은 상황이 생겼다. 아무생각 없이 코드를 치다가 순간 섬뜩해서 영한님 JPA 강의 자료를 뒤져보니 역시.. fetch join의 대상에 별칭을 주고 where 절에서 필터링하는건 불가능하다고 되어있었다.&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;fetch join은 별칭을 아예 줄 수 없나?&lt;/li&gt;
&lt;li&gt;where 절에 XToOne 쪽 필드를 조건으로 주는 경우는 어떻게 동작하나?&lt;/li&gt;
&lt;li&gt;fetch join 말고 join은 사용 해도 되는건가 그럼?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우에는 어떻게 동작하지?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애초에 fetch join을 왜 where 절과 함께 사용하면 안 되는거지..?&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의 어려움에 멘붕이 올뻔했지만 마음을 추스리고 몇 가지로 경우의 수를 추려서 JPA가 어떤 식으로 동작하는지 직접 테스트해보기로 했다.  &lt;/p&gt;
&lt;h2 id=&quot;테스트용-엔티티&quot; data-ke-size=&quot;size26&quot;&gt;테스트용 엔티티&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 일대다 관계를 갖는 Parent와 Child 엔티티를 사용해 진행했다.&lt;/p&gt;
&lt;h4 id=&quot;다이어그램&quot; data-ke-size=&quot;size20&quot;&gt;다이어그램&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjZTea/btsslaPVqPD/03zyA89Y0vyKCKZXsyXHLk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;parent-one&quot; data-ke-size=&quot;size20&quot;&gt;Parent (One)&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Parent {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = &quot;parent&quot;)
    private List&amp;lt;Child&amp;gt; childList = new ArrayList&amp;lt;&amp;gt;();

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;child-many&quot; data-ke-size=&quot;size20&quot;&gt;Child (Many)&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Child {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Parent parent;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 테스트 데이터를 넣어준 상태이다.&lt;/p&gt;
&lt;h4 id=&quot;테스트-데이터&quot; data-ke-size=&quot;size20&quot;&gt;테스트 데이터&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDMRIc/btssqAf6414/ICbajzLnO1mgnOjcp0OPek/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;테스트-결과&quot; data-ke-size=&quot;size26&quot;&gt;테스트 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 해보니, fetch join + where 절 조합이라고 해서 무조건 사용이 불가능한 것은 아니었고 사용이 가능한 상황도 있었다. 그리고 fetch join과 where 절을 같이 사용하면 안 되는 이유에 대해서도 오해를 하고 있었다. 아래에서 각 경우 별로 어떻게 동작하는지, 왜 사용하면 안 되는지 등 테스트를 통해 알아낸 것들을 공유해보려고 한다.&lt;/p&gt;
&lt;h3 id=&quot;fetch-join--xtoone-필드에-where-조건&quot; data-ke-size=&quot;size23&quot;&gt;fetch join + XToOne 필드에 where 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, XToOne의 필드를 where 절의 조건으로 사용하는 fetch join 는 사용해도 괜찮다. 아래 코드와 같은 경우이다. (where 절은 각 경우마다 동일하게 Parent 나 Child의 이름에 &lt;code&gt;A&lt;/code&gt; 가 들어간 row만 조회하도록 조건을 걸어주었다.)&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Repository
public class ChildRepository {
    private final EntityManager em;

    public List&amp;lt;Child&amp;gt; fetchJoinToOne() {
        return em.createQuery(
                &quot;select c from Child c &quot; +
                        &quot;join fetch c.parent p &quot; +
                        // Child(M)을 조회할 때 Parent(1)에 조건을 줌
                        &quot;where p.name like '%A%'&quot;, Child.class
        ).getResultList();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 돌려보면 다음과 같이 기대한대로 Parent A를 가진 Child A와 Child B만 포함된 리스트가 조회되고, 각 Child은 Parent A를 제대로 가지고 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/7zgoY/btssBo6tgM9/Dcb2x7V6GgqZ5BVAwjcZCK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/F7gVJ/btssxGtaKUs/wurhde0LYW1I0T8ILYkQIk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림으로 보면 다음과 같다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXflMM/btssvZflloo/3c4E3v6V9QknorOCzms4P1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리도 fetch join을 사용했으므로 다음과 같은 하나의 쿼리로 모든 데이터를 가져오게 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbnGZc/btsswVcGsv2/UJdZidIwKZW2ig2nqqfpZk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h3 id=&quot;일반-join--xtoone-필드에-where-조건&quot; data-ke-size=&quot;size23&quot;&gt;일반 join + XToOne 필드에 where 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XToOne의 필드를 where 절의 조건으로 사용하는 일반 join 또한 (성능 문제를 제외하면) 사용해도 괜찮다. 다음과 같이 코드를 작성하고 테스트를 돌려보면 잘 통과하게 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public List&amp;lt;Child&amp;gt; joinToOne() {
    return em.createQuery(
            &quot;select c from Child c &quot; +
                    &quot;join c.parent p &quot; +
                    &quot;where p.name like '%A%'&quot;, Child.class
    ).getResultList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hxqss/btssijeXxQg/TmNUa2RNFR3loZ2T9VqH2K/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(LAZY 로딩으로 인해 Parent에서 Child를 꺼내오는 시점이 트랙잭션 내부여야 한다)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/YnWDe/btssBYmfQs8/NnRjKfqzQkKQMCB0br771K/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이 방식의 문제는 당연하게도 쿼리가 N + 1 번 나간다는 것이다.&lt;/p&gt;
&lt;h4 id=&quot;child-select-쿼리&quot; data-ke-size=&quot;size20&quot;&gt;Child select 쿼리&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXWg69/btssqB0ovvV/pKfvai8OJDqwvBoWYEoLBk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;child가-가진-parent-select-쿼리&quot; data-ke-size=&quot;size20&quot;&gt;Child가 가진 Parent select 쿼리&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/chWrpR/btssuxjqJf3/KVvVWWdAMqIwQCPddoGeKK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 먼저 XToOne의 경우 일반 join 대신, fetch join을 (where 조건과 함께) 사용해도 괜찮다는 결론을 내릴 수 있었다.&lt;/p&gt;
&lt;h3 id=&quot;fetch-join--xtomany-필드에-where-조건&quot; data-ke-size=&quot;size23&quot;&gt;fetch join + XToMany 필드에 where 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음부터는 XToMany의 경우이다. 먼저 fetch join인데 where 절 조건을 XToMany 측 필드에 거는 경우이다. 즉 아래 코드와 같은 상황이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Repository
public class ParentRepository {
    private final EntityManager em;

    public List&amp;lt;Parent&amp;gt; fetchJoinToMany() {
        return em.createQuery(
                &quot;select p from Parent p &quot; +
                        &quot;join fetch p.childList c &quot; +
                        // Parent(1)을 조회할 때 Child(M)에 조건을 줌
                        &quot;where c.name like '%A%'&quot;, Parent.class
        ).getResultList();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 직접 위와 같은 쿼리를 날리면 아래와 같은 모양의 결과를 기대할 것이다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/OYhUB/btssk9Dvd7A/bgmsRukoDKGdZhcrpwkuCk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제로도 그러한 결과가 나오게 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAKuBK/btssvVquiO4/fUBkmJjY4kfk6wlm36mxy1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/LYuYg/btssCuk32WD/eKsO4cXaPwfxpp9aa4n7w0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 엥? fetch join 랑 where 조건을 같이 사용하면 안 된다고 하지 않았던가? 하는 혼란이 왔다. 확인해보니 위의 테스트 결과처럼 원래 fetch join + where 절 조건을 주면 DB에 쿼리를 직접 날렸을 때 기대한 것과 동일한 결과가 나오는게 맞다. 그런데 문제는 Repository에서 반환하는게 &lt;b&gt;엔티티&lt;/b&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;현재 ParentA와 연관된 Child들의 DB에서의 상태는 아래와 같은데,&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/z2Fri/btssqnuBamj/zYHiqXoBHoSwRQxELhYgn0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회한 엔티티의 상태는 다음과 같다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/ra4aI/btssih9gVrG/aTproRdgzB7K9zdmw2dvwK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 DB의 상태와 객체의 상태를 항상 동일하게 유지해준다. 따라서 개발자도 이 점을 고려하며 코드를 작성하게 된다. 하지만 위와 같이 fetch join을 사용하게 되면, DB와 객체의 상태가 불일치하게 되고 아래와 같이 개발자가 현 객체의 상태가 DB와 같다는 것을 가정하고 코드를 작성했을 때 문제가 발생할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQSd9y/btssvWbP0kg/nGSbeg8ObhtUKjupmWXYq1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 영속성 컨텍스트에 Child B가 존재하지 않는 Parent A가 올라와 있기 때문에, findAll()로 모든 Parent를 조건 없이 조회한다고 해도 Child B는 Parent A의 childList에 포함되지 않게 되고, 테스트가 통과하지 못하게 된다.&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;Parent A에 당연히 Child B가 존재할 것이라고 생각하고 코드를 작성한다면 의도하지 않은 결과가 나오게 될 것이다. &lt;b&gt;즉, fetch join + where 조건을 사용하면 안 되는 이유는 원하는 데이터가 조회되지 않아서 라기 보다는 DB와 객체의 일관성이 깨짐으로 인해 DB의 데이터가 우리가 의도하지 않게 수정될 수 있기 때문이었던 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/vRlVq/btssuvlyQae/K9nO2iErW2d5nRzb1KwdvK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 경우(fetch join 사용)에는 아래와 같이 쿼리 하나로 데이터를 모두 가져올 수 있긴 하다. (하지만 사용하면 위험하다!)&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBAxNg/btssAJC7rdT/ibKKDhL945txsK1pt9CDH1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h3 id=&quot;일반-join--xtomany-필드에-where-조건&quot; data-ke-size=&quot;size23&quot;&gt;일반 join + XToMany 필드에 where 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 XToMany 필드에 where 조건을 건 일반 join은 어떻게 동작할까?&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public List&amp;lt;Parent&amp;gt; joinToMany() {
    return em.createQuery(
            &quot;select p from Parent p &quot; +
                    &quot;join p.childList c &quot; +
                    &quot;where c.name like '%A%'&quot;, Parent.class
    ).getResultList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 돌려보면, 실패하게 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/GOpC0/btssk2jJ6iG/mIXojRnAI4DwuQOsG9xCQk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/JDwMB/btssBmVdlAA/nKdokonOcRVTKkUBMMMwk1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대한 결과는 다음과 같은데,&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/beqo75/btssB0qU07L/cdSMkRegQWkwwC0Z9hkRu1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 아래와 같이 Child A, Child B가 모두 조회 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/qLDKD/btssikL1jZd/JkPws5buNLJ92oxfS0Koq0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 join 시 발생한 쿼리를 보면 그 이유를 알 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/YK9D4/btssimiKZaI/i8uuHOX8VcsJWTtPrhHMk1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리가 총 2번 (1 + N번) 나가게 되는데 쿼리를 살펴보면 처음에 Parent를 조회할 때 Child 이름에 조건을 걸어서 조회하기 원하는 Child의 Parent만을 선택하긴 하지만, &lt;b&gt;Child가 LAZY 로딩이기 때문에 이후에 Child를 별도의 쿼리로 다시 조회&lt;/b&gt;하고 되고 이때는 &lt;code&gt;Child.name&lt;/code&gt; 조건이 걸리지 않고 단순히 앞서 조회된 &lt;b&gt;Parent의 id만을 조건으로 모든 Child를 조회&lt;/b&gt;하게 된다. 따라서, 위와 같은 (의도하지 않게 모든 Child가 Parent의 childList에 포함된) 결과가 나오게 된다.&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;즉, 일반 join을 사용하는 경우 DB와의 일관성 문제는 없으나 만약 ChildList에 Child A만이 존재하는 결과를 원했던 경우 의도치 않게 Child B까지 조회되는 상황이 발생한다.&lt;/p&gt;
&lt;h3 id=&quot;dto-사용-join-xtomany의-대안&quot; data-ke-size=&quot;size23&quot;&gt;DTO 사용 (join XToMany의 대안)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 결과만 얻으면서도 DB - 객체 간 데이터 일관성을 깨뜨리지 않는 방법은 DTO를 사용해 데이터를 직접 조인 후 조회하는 것이다. 먼저 아래와 같이 데이터를 담을 DTO를 생성한다.&lt;/p&gt;
&lt;h4 id=&quot;parentquerydto&quot; data-ke-size=&quot;size20&quot;&gt;ParentQueryDto&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Data
public class ParentQueryDto {

    private Long id;
    private String name;
    private List&amp;lt;ChildQueryDto&amp;gt; childQueryDtos;

    public ParentQueryDto(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;childquerydto&quot; data-ke-size=&quot;size20&quot;&gt;ChildQueryDto&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Data
public class ChildQueryDto {
    private Long parentId;
    private Long id;
    private String name;

    public ChildQueryDto(Long parentId, Long id, String name) {
        this.parentId = parentId;
        this.id = id;
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;parentqueryrepository&quot; data-ke-size=&quot;size20&quot;&gt;ParentQueryRepository&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Repository
public class ParentQueryRepository {

    private final EntityManager em;

    public List&amp;lt;ParentQueryDto&amp;gt; findAllByDtos() {
        List&amp;lt;ParentQueryDto&amp;gt; parents = findParents();

        Map&amp;lt;Long, List&amp;lt;ChildQueryDto&amp;gt;&amp;gt; childMap = findChildMap();
        parents.forEach(parent -&amp;gt; parent.setChildQueryDtos(childMap.get(parent.getId())));

        return parents;
    }

    private List&amp;lt;ParentQueryDto&amp;gt; findParents() {
        return em.createQuery(
                &quot;select new jpabook.jpashop.repository.ParentQueryDto(p.id, p.name) &quot; +
                        &quot;from Parent p &quot; +
                        &quot;join p.childList c &quot; +
                        &quot;where c.name like '%A%'&quot;, ParentQueryDto.class)
                .getResultList();
    }

    private Map&amp;lt;Long, List&amp;lt;ChildQueryDto&amp;gt;&amp;gt; findChildMap() {
        List&amp;lt;ChildQueryDto&amp;gt; childs = em.createQuery(
                        &quot;select new jpabook.jpashop.repository.ChildQueryDto(p.id, c.id, c.name) &quot; +
                                &quot;from Child c join c.parent p &quot; +
                                &quot;where c.name like '%A%'&quot;, ChildQueryDto.class
                )
                .getResultList();

        return childs.stream()
                .collect(groupingBy(ChildQueryDto::getParentId));
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 간단히 설명하면, 먼저 findAllByDto() 에서 findParents() 를 호출해 Child의 name에 조건을 걸고 Parent를 조회한 결과를 얻는다. 그리고 findChildMap() 메서드를 호출해 동일한 where 절로 Child를 조회하고, 조회결과를 &lt;code&gt;Parent.ID&lt;/code&gt;를 key로, &lt;code&gt;List&amp;lt;Child&amp;gt;&lt;/code&gt;를 value로 갖는 Map을 만들어 반환하도록 한다. 마지막으로 이를 forEach로 순회하며 key에 해당하는 Parent의 ChildList에 Map value인 List를 넣어준다.&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;테스트 해보면 아래와 같이 원하는 결과를 얻은 것을 확인할 수 있다. 또한 엔티티가 아닌 DTO로 조회했기 때문에 데이터 일관성 깨짐으로 인한 문제도 발생하지 않게된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zs9t0/btsslah4ZQw/CBr0sJntnte6QGUYjuvWJk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;조회-결과&quot; data-ke-size=&quot;size20&quot;&gt;조회 결과&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/3jsiJ/btsshoOmJqV/7zTROzZDjlNl15eEhzVYi0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;테스트-결과-1&quot; data-ke-size=&quot;size20&quot;&gt;테스트 결과&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/kSGIM/btssvvFEEmO/WuAD5Dk1FS5gkKf6L8DCtk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 이 경우 쿼리는 다음과 같이 2번(O(1)) 발생하게 된다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/diKnY5/btssv0ef6I3/OVuGNZWw7vhACbJ4NNRFt1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;결론&quot; data-ke-size=&quot;size26&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, XToOne 측에 조건을 주고자 하는 경우 fetch join을 사용해도 괜찮다. 또 만약 특정 Child 조건을 만족하는 'Parent' 객체만 필요한 경우라면, 일반 join XToMany를 사용하면 될듯하다. 이 경우엔 (Child를 사용하지 않으므로) 추가 쿼리도 발생하지 않고, 데이터 일관성도 깨지지 않는다.&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;만약 그게 아니라면 (Child 에 조건을 주면서 Child의 데이터도 필요한 경우) DTO를 사용해 필요한 필드를 모아서 한번에 join으로 조회해야 한다. 왜냐하면 그렇게 해야 데이터의 일관성이 깨지지 않아 DB에 의도치 않은 값이 들어가거나 다른 값이 조회될 염려가 없으며 정말 조회하기 원했던 Many 측 데이터만 조회하여 사용할 수 있기 때문이다. DTO를 통해 직접 join을 수행하게 되면 N + 1 쿼리도 발생하지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 테스트를 통해 작성한 내용이라 잘못된 내용이 있을 수 있습니다.&lt;br /&gt;혹시 잘못된 내용이 있다면 댓글로 알려주시면 확인하고 수정하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;참고-자료&quot; data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-API%EA%B0%9C%EB%B0%9C-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94&quot;&gt;실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 강의&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/questions/15876/fetch-join-%EC%8B%9C-%EB%B3%84%EC%B9%AD%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4&quot;&gt;fetch join 시 별칭관련 질문입니다 - 인프런 | 질문 &amp;amp; 답변&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/questions/59632/fetch-join-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8-%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4&quot;&gt;fetch join 관련 질문 드립니다!! - 인프런 | 질문 &amp;amp; 답변&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/article&gt;</description>
      <category>개발 공부/Spring</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/138</guid>
      <comments>https://gmelon.dev/138#entry138comment</comments>
      <pubDate>Tue, 29 Aug 2023 19:58:35 +0900</pubDate>
    </item>
    <item>
      <title>[플랭고] 일대일에서 일대다로 변경 시 validation 관련 문제 (Custom ConstraintValidator)</title>
      <link>https://gmelon.dev/122</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;h2 id=&quot;문제-상황&quot; data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 중인 애플리케이션 플랭고의 '기록하기' 기능은 현재 1장의 이미지만 첨부할 수 있다. 이를 20장까지 첨부할 수 있도록 변경하는 작업을 진행했다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceXHiZ/btsBKk1z1N4/yBHXAtuw4jrDQjJ0Y3gj01/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사진은 2023 인프콘  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 다음과 같은 작업들을 진행했다.&lt;/p&gt;
&lt;h4 id=&quot;1-이미지-url을-저장할-diaryimage-테이블-엔티티-생성&quot; data-ke-size=&quot;size20&quot;&gt;1. 이미지 url을 저장할 DiaryImage 테이블, 엔티티 생성&lt;/h4&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = PROTECTED)
@Entity
public class DiaryImage extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = &quot;diary_id&quot;)
    private Diary diary;

    private String imageUrl;

    @Builder
    public DiaryImage(Diary diary, String imageUrl) {
        this.diary = diary;
        this.imageUrl = imageUrl;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-diary기록-엔티티와-diaryimage-엔티티를-일대다로-연관관계-설정&quot; data-ke-size=&quot;size20&quot;&gt;2. Diary(기록) 엔티티와 DiaryImage 엔티티를 일대다로 연관관계 설정&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = PROTECTED)
@Entity
public class Diary extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @Column(columnDefinition = &quot;TEXT&quot;)
    private String content;

    // 기존 필드
    // private String imageUrl;

    // 변경된 필드
    @OneToMany(mappedBy = &quot;diary&quot;, cascade = ALL, orphanRemoval = true)
    private List&amp;lt;DiaryImage&amp;gt; diaryImages = new ArrayList&amp;lt;&amp;gt;();

    /* 생략 */

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-요청-및-응답-dto의-필드를-list로-변경&quot; data-ke-size=&quot;size20&quot;&gt;3. 요청 및 응답 DTO의 필드를 List로 변경&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@NoArgsConstructor
@Getter
public class DiaryCreateRequestDto {

    private String content;

    // 기존
    // @URL
    // private String imageUrl;

    // 변경
    @URL
    private List&amp;lt;String&amp;gt; imageUrls = new ArrayList&amp;lt;&amp;gt;();

    /* 생략 */

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@NoArgsConstructor
@Getter
public class DiaryResponseDto {

    private Long id;

    private String content;

    // 기존
    // private String imageUrl;

    // 변경
    private List&amp;lt;String&amp;gt; imageUrls = new ArrayList&amp;lt;&amp;gt;();

    /* 생략 */

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-기존에-하나의-파일만-업로드가-가능했던-s3-관련-api도-여러-파일을-한번에-처리할-수-있도록-변경&quot; data-ke-size=&quot;size20&quot;&gt;4. 기존에 하나의 파일만 업로드가 가능했던 S3 관련 api도 여러 파일을 한번에 처리할 수 있도록 변경&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@RequestMapping(&quot;/api/s3&quot;)
@RestController
public class S3Controller {

    private final S3Service s3Service;

    @PostMapping
    public FileUploadResponseDto uploadAll(@RequestParam List&amp;lt;MultipartFile&amp;gt; files) {
        validateFilesAreNotEmpty(files);
        return s3Service.uploadAll(files);
    }

    /* 생략 */

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이렇게 변경하고 테스트를 실행해보니 예상하지 못한 곳에서 다음과 같은 오류가 발생했다.&lt;/p&gt;
&lt;h4 id=&quot;테스트-결과&quot; data-ke-size=&quot;size20&quot;&gt;테스트 결과&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpyv0W/btsBCY7i7Fg/v05OK1Te9FyZjKR8U5M51k/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;디버깅-결과&quot; data-ke-size=&quot;size20&quot;&gt;디버깅 결과&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/veiWH/btsBC6dgO9z/XAxeQ0aegyD4gKxHxQVLs0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;원인&quot; data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;h4 id=&quot;오류-메시지&quot; data-ke-size=&quot;size20&quot;&gt;오류 메시지&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;No validator could be found for constraint 'javax.validation.constraints.Pattern' validating type 'java.util.List'. Check configuration for 'imageUrls'&lt;/p&gt;
&lt;/blockquote&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에 대해 Pattern validation을 수행할 validator가 없다고 한다. 기존 한 개짜리 String 타입의 imageUrl을 List 타입의 imageUrls로 변경하면서 당연히 우리의 스프링 부트 또는 다른 누군가(?)가 리스트가 있으면 해당 리스트의 원소들을 순회하며 validation을 수행해주겠지 라고 생각한게 잘못이었다.&lt;/p&gt;
&lt;h4 id=&quot;문제가-되는-부분&quot; data-ke-size=&quot;size20&quot;&gt;문제가 되는 부분&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/kXxkh/btsBDL7OrId/zakKs9cCK406xmekZyoopk/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 List에 대해서는 별도로 원소가 없어야 한다거나 몇 개만 있어야 한다거나 등의 검증을 수행할 수도 있기 때문에 자동으로 내부 원소를 순회하며 검증을 해주는 것도 문제가 될 여지가 있어보였다.&lt;/p&gt;
&lt;h2 id=&quot;해결-방법&quot; data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 기존에 사용했던 &lt;a href=&quot;https://github.com/URL&quot;&gt;@URL&lt;/a&gt; 대신, List를 타겟으로 하여, 해당 List의 원소를 순회하며 &lt;a href=&quot;https://github.com/URL&quot;&gt;@URL&lt;/a&gt;과 동일한 검증 과정을 수행하는 Custom Validator를 만들어 사용하기로 했다.&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;a href=&quot;https://docs.spring.io/spring-framework/docs/3.0.0.RC2/reference/html/ch05s07.html&quot;&gt;스프링 문서&lt;/a&gt;에 보면 &lt;code&gt;ConstraintValidator&lt;/code&gt; 를 구현하고 Custon 검증 어노테이션에 해당 클래스를 value로 갖는 &lt;a href=&quot;https://github.com/Constraint&quot;&gt;@Constraint&lt;/a&gt; 메타 어노테이션을 달아주면 동일하게 검증을 할 수 있다고 되어있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/UuNSV/btsBC2vfT4t/4GheaILaok33oJhNl0KWf1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같이 &lt;code&gt;ConstraintValidator&lt;/code&gt;를 implements하는 &lt;code&gt;CollectionURLValidator&lt;/code&gt; 를 구현해주었다. 이 validator는 Collection의 원소들을 하나하나 순회하며 &lt;a href=&quot;https://github.com/URL&quot;&gt;@URL&lt;/a&gt; 이 수행하는 것과 동일한 검증을 수행하고, 하나의 원소라도 검증에 실패할 경우 false를 반환해준다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;public class CollectionURLValidator implements ConstraintValidator&amp;lt;CollectionURL, Collection&amp;lt;? extends CharSequence&amp;gt;&amp;gt; {

    private String protocol;
    private String host;
    private int port;

    @Override
    public void initialize(CollectionURLValidation annotation) {
        this.protocol = annotation.protocol();
        this.host = annotation.host();
        this.port = annotation.port();
    }

    @Override
    public boolean isValid(Collection&amp;lt;? extends CharSequence&amp;gt; values, ConstraintValidatorContext context) {
        if (values == null) {
            return true;
        }

        for (CharSequence value : values) {
            if (!isValid(value)) {
                return false;
            }
        }
        return true;
    }

    private boolean isValid(CharSequence value) {
        if ( value == null || value.length() == 0 ) {
            return true;
        }

        URL url;
        try {
            url = new URL( value.toString() );
        }
        catch (MalformedURLException e) {
            return false;
        }

        if ( protocol != null &amp;amp;&amp;amp; protocol.length() &amp;gt; 0 &amp;amp;&amp;amp; !url.getProtocol().equals( protocol ) ) {
            return false;
        }

        if ( host != null &amp;amp;&amp;amp; host.length() &amp;gt; 0 &amp;amp;&amp;amp; !url.getHost().equals( host ) ) {
            return false;
        }

        if ( port != -1 &amp;amp;&amp;amp; url.getPort() != port ) {
            return false;
        }

        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConstraintValidator&amp;lt;A, T&amp;gt; 클래스의 docs를 보면 다음과 같이 타입 파라미터 &lt;b&gt;A&lt;/b&gt;에 이 Validator에 의해 처리될 어노테이션 타입을 지정하고 타입 파라미터 &lt;b&gt;T&lt;/b&gt;에 이 Validator가 지원하는 타겟 타입을 적으라고 되어있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdmLM3/btsBC5rS3x6/o36NqFwgf6TVYLcbypUty1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 &lt;b&gt;A&lt;/b&gt;에는 DTO에 새롭게 달아줄 커스텀 검증 어노테이션 CollectionURL을, &lt;b&gt;T&lt;/b&gt;에는 CharSequence과 그 하위로 타입을 제한한 한정적 와일드카드를 사용하는 Collection을 넣어주었다.&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;커스텀 validator인 CollectionURLValidator의 내부 코드는 Collection을 순회한다는 점을 제외하면 기존 &lt;a href=&quot;https://github.com/URL&quot;&gt;@URL&lt;/a&gt;의 기본 Validator인 hibernate의 URLValidator의 코드를 참고해서 비슷하게 작성해주었다.&lt;/p&gt;
&lt;h4 id=&quot;hibernate의-urlvalidator&quot; data-ke-size=&quot;size20&quot;&gt;hibernate의 URLValidator&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/bujfyF/btsBHQTYHyl/yGfi5eoHOHdyvZHej9U2r1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 검증 어노테이션 CollectionURL 역시 URL에서 사용하지 않는 정보를 제거하고 &lt;a href=&quot;https://github.com/Constraint&quot;&gt;@Constraint&lt;/a&gt; 메타어노테이션의 value를 직접 만든 validator로 변경하는 정도로 비슷하게 구현할 수 있었다.&lt;/p&gt;
&lt;h4 id=&quot;커스텀-검증-어노테이션-collectionurl&quot; data-ke-size=&quot;size20&quot;&gt;커스텀 검증 어노테이션 CollectionURL&lt;/h4&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Constraint(validatedBy = CollectionURLValidator.class)
public @interface CollectionURL {

    String message() default &quot;올바른 URL이 아닙니다.&quot;;

    Class&amp;lt;?&amp;gt;[] groups() default {};

    Class&amp;lt;? extends Payload&amp;gt;[] payload() default {};

    String protocol() default &quot;&quot;;

    String host() default &quot;&quot;;

    int port() default -1;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 커스텀 Validator와 어노테이션을 구현하고 다음과 같이 DTO를 수정해주니 이제 테스트가 잘 통과되는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/czWwWx/btsBKk8kFuT/IUm3hiJ3T3LmMvdNdMOX60/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/pcJWm/btsBFHqkz2G/8kXnKXjHk2JtFaszUZktpK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;아쉬운-점&quot; data-ke-size=&quot;size26&quot;&gt;아쉬운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 아쉬운 점이라면, 한창 인프런에서 토비님의 스프링 부트 강의를 듣고 있던 중이라 어노테이션으로 무언가 동작하게 만드는 일이 너무 신기하고 재밌어 보여서 Collection 검증 어노테이션도 여러 기존 검증 어노테이션과 결합해 범용으로 사용할 수 있도록 구현하고 싶었는데 실패했다는 점이다.  &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;대략 아래 코드처럼 Collection의 검증을 쉽게 할 수 있도록 만들고 싶어서 엄청 길게 validator 코드를 작성하다가 스프링에서 제공하는 클래스들에 대한 이해가 부족해서 계속.. 계속.. 실패하고 우선 나중으로 미루기로 했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;@CollectionValidation(constraints = {URL.class, ...})
private List&amp;lt;String&amp;gt; imageUrls = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 이런 비슷한 상황이 됐을 때 꼭 다시 시도해보고 싶다.&lt;/p&gt;
&lt;h2 id=&quot;참고자료&quot; data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/3.0.0.RC2/reference/html/ch05s07.html&quot;&gt;5.7&amp;nbsp;Spring 3 Validation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/article&gt;</description>
      <category>개발 공부/Spring</category>
      <category>CollectionValidation</category>
      <category>ConstraintValidator</category>
      <category>Validation</category>
      <category>스프링 DTO Validation</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/122</guid>
      <comments>https://gmelon.dev/122#entry122comment</comments>
      <pubDate>Wed, 23 Aug 2023 01:08:06 +0900</pubDate>
    </item>
    <item>
      <title>[플랭고] 스프링 시큐리티 실패 핸들러 에서 직접 예외를 던지면 안 되는 이유</title>
      <link>https://gmelon.dev/121</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot;&gt;
&lt;h2 id=&quot;문제-상황&quot; data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 중인 프로젝트에서 json 형태로 로그인을 처리하기 위해 AbstractAuthenticationProcessingFilter을 상속받는 커스텀 AuthenticationFilter를 구현하고 시큐리티에 필터로 등록해 사용하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 커스텀 Json 로그인 처리 필터를 구현하고&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class JsonEmailPasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    ...

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
    ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 시큐리티 설정을 작성했다. 로그인 실패 성공 시에 응답 처리도 json으로 내려주기 위해 Success, Failure Handler도 같이 등록해주었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Bean
public JsonEmailPasswordAuthenticationFilter jsonEmailPasswordAuthenticationFilter() {
    JsonEmailPasswordAuthenticationFilter filter = new JsonEmailPasswordAuthenticationFilter(&quot;/api/auth/login&quot;, objectMapper);
    filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
    filter.setRememberMeServices(getRememberMeServices());
    // 여러가지 핸들러 등록
    filter.setAuthenticationManager(authenticationManager());
    filter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
    filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());

    return filter;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 뭔가 로직이 실패했을 때 이 핸들러들에서 Exception을 던져주기만 하면 &lt;a href=&quot;https://github.com/RestControllerAdvice&quot;&gt;@RestControllerAdvice&lt;/a&gt; 어노테이션이 지정된 ExceptionHandler 클래스에서 이를 처리해줄 것이라고 기대했다. 그래서 예를 들어 로그인 실패 시 호출되는 CustomAuthenticationFailureHandler의 경우 아래와 같이 작성했었다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        throw new UsernameNotFoundException(ErrorMessages.LOGIN_FAILURE_ERROR_MESSAGE);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드도 간단하고 ExceptionHandler 클래스 한 곳에서 예외를 처리하므로 깔끔하군 이라고 생각하고 테스트를 돌려봤는데 ExceptionHandler에서 작성한 응답이 아니라 스프링 기본 예외 응답이 출력되는 것을 확인했다.&lt;/p&gt;
&lt;h4 id=&quot;기대한-것&quot; data-ke-size=&quot;size20&quot;&gt;기대한 것&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/p3FZa/btsBF19TLBs/24CaF3q6bUxAnFicVxspM0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h4 id=&quot;실제-출력&quot; data-ke-size=&quot;size20&quot;&gt;실제 출력&lt;/h4&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ964K/btsBDKHNUh4/VjIUMuhfUrqnXYCPFQLIS0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;원인&quot; data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 로그인을 시도하는 필터가 말그대로 '필터'이기 때문이었다. WAS에 들어온 요청은 다음과 같은 순서로 전달되는데&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/821Go/btsBFfU9b3j/aHt6ZMYjQLfi59SdFqb0eK/img.png&quot; alt=&quot;&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;ExceptionHandler 등 처리 로직은 스프링의 DispatcherServlet에서 핸들러를 호출한 후 예외가 발생했을 때 해당 예외 처리 핸들러를 호출해주는 것이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿(DispatcherServlet)까지도 전달되지 못하고 &lt;b&gt;필터에서 예외가 발생&lt;/b&gt;했으니, 스프링의 기능을 통해 만들어진 ExceptionHandler에서는 이를 알아차릴 방법이 없는 것이다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// DispatcherServlet.doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

    // ...

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {

            // ...

            // 매핑된 핸들러(컨트롤러) 호출
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            // ...

        catch (Exception ex) {
            // 컨트롤러에서 발생한 예외가 있다면 catch
            dispatchException = ex;
        }

        // ...

        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

        // ...

}

// DispatcherServlet.processDispatchResult()
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
            @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
            @Nullable Exception exception) throws Exception {

        boolean errorView = false;

        if (exception != null) {

            // ...

            else {
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                // processHandlerException() 이 핸들러(컨트롤러)에서 발생한 예외를 처리
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }

        // ...

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;서블릿의-예외-처리-로직&quot; data-ke-size=&quot;size23&quot;&gt;서블릿의 예외 처리 로직&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 기본 WAS의 예외 처리 로직에 따라 동작하게 된다. WAS는 WAS에 예외가 도달하면 WAS는 해당 예외와 매핑된 url이 있는지 확인하고 해당 주소로 다시 ERROR 요청을 날리는 방식으로 예외를 처리한다.&lt;/p&gt;
&lt;h3 id=&quot;스프링부트의-기본-예외-처리-컨트롤러&quot; data-ke-size=&quot;size23&quot;&gt;스프링부트의 기본 예외 처리 컨트롤러&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트는 기본적으로 아래와 같이 예외에 대해 url을 등록하고&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// ErrorMvcAutoConfiguration.ErrorPageCustomizer
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

    @Override
    public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        ErrorPage errorPage = new ErrorPage(
                this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())); // 기본은 /error
        errorPageRegistry.addErrorPages(errorPage);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 url에 대해 기본 예외 응답을 내려주는 컨트롤러를 다음과 같이 제공한다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@RequestMapping(&quot;${server.error.path:${error.path:/error}}&quot;)
public class BasicErrorController extends AbstractErrorController {

    // ...

    @RequestMapping
    public ResponseEntity&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity&amp;lt;&amp;gt;(status);
        }
        Map&amp;lt;String, Object&amp;gt; body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity&amp;lt;&amp;gt;(body, status);
    }

    // ...

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 시큐리티 필터 핸들러에서 바로 예외를 내렸을 때 처음에 본 스프링의 기본 예외 응답이 내려왔던 것이다. 보면 처음에 날린 요청이 아닌 ERROR 타입의 요청이 스프링부트의 BasicErrorController에 전달된 것을 확인할 수 있다.&lt;/p&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dn/m2VIb/btsBJco3PhJ/jdyy62UkBQs6dVHzVyN2D1/img.png&quot; alt=&quot;&quot; /&gt;
&lt;h2 id=&quot;해결-방법&quot; data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 해결 방법은 시큐리티 필터에서 호출하는 핸들러에서 Exception을 던지지 말고 바로 원하는 응답을 만들어서 반환하면 된다. 아래와 같이 ObjectMapper와 Response의 Writer 객체를 이용해 응답에 원하는 값을 바로 작성해주었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// CustomAuthenticationFailureHandler 클래스
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    ErrorResponseDto errorResponse = ErrorResponseDto.builder()
            .message(ErrorMessages.LOGIN_FAILURE_ERROR_MESSAGE)
            .build();

    response.setContentType(APPLICATION_JSON_VALUE);
    response.setCharacterEncoding(UTF_8.name());
    response.setStatus(SC_BAD_REQUEST);

    objectMapper.writeValue(response.getWriter(), errorResponse);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 예외가 발생한 필터에서 작성된 응답이 호출됐던 필터를 타고 다시 WAS로 전달되어 클라이언트에게 원하는 응답이 내려진다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티의 로그인 처리는 필터에서 수행되므로 스프링에서 제공하는 ExceptionHandler 를 사용할 수 없다! 직접 응답을 만들어 Response 객체에 작성해주자.&lt;/p&gt;
&lt;/article&gt;</description>
      <category>개발 공부/Spring</category>
      <category>AuthenticationFailureHandler</category>
      <category>시큐리티</category>
      <category>예외처리</category>
      <category>핸들러</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/121</guid>
      <comments>https://gmelon.dev/121#entry121comment</comments>
      <pubDate>Sun, 20 Aug 2023 21:19:50 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - 구현] 자물쇠와 열쇠</title>
      <link>https://gmelon.dev/118</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;☀️-시작&quot;&gt;☀️ 시작&lt;/h2&gt;
&lt;p&gt;이번 문제는 2020 카카오 신입 개발자 블라인드 채용 1차 코딩 테스트의 3번 문항 '자물쇠와 열쇠' 문제이다. 카카오 해설을 보면 출제 의도는 &lt;strong&gt;2차원 배열을 다룰 수 있는가&lt;/strong&gt; 이다.&lt;/p&gt;
&lt;h2 id=&quot; -문제&quot;&gt;  문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/60059&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이 문제는 MxM 크기의 key 배열과 NxN 크기의 lock 배열이 주어지는데 M &amp;lt;= N 이고, key 배열과 lock 배열을 일부 또는 전체 겹쳐서 &lt;strong&gt;lock 배열의&lt;/strong&gt; 모든 좌표의 각각의 합이 1이 될 수 있는지를 구하는 문제이다. (기준은 lock이다) 이때 key 배열은 90도씩 4방향 모두 회전시킬 수 있다.&lt;/p&gt;
&lt;p&gt;문제에서는 자물쇠의 모든 홈에 열쇠의 돌기를 맞출 수 있는지 물어보고 있고, 홈을 0, 돌기를 1로 표현한다.&lt;/p&gt;
&lt;p&gt;즉, 아래와 같은 lock이 있다면&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFlmkZ/btslCb8aKFO/rieonCfbR9U0qOZ0iRKhlK/img.png&quot; alt=&quot;image-20230627214454457&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래와 같이 key를 180도 돌리고 lock과 일부 겹치면 겹쳐진 key와 lock의 모든 좌표에서의 합이 각각 1이 되므로 자물쇠를 풀 수 있고, true를 반환하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsHUB1/btslB4gZk0f/BlQtkaXXxMgenuSsdAAxkk/img.png&quot; alt=&quot;image-20230627214529328&quot; /&gt;&lt;/p&gt;
&lt;p&gt;만약 어떤 방향으로 회전시키고 어떤 부분을 겹쳐도 모든 좌표의 합이 각각 정확히 1이 되지 못하면 자물쇠를 풀 수 없는 것으로, false를 반환하면 된다.&lt;/p&gt;
&lt;h2 id=&quot; ‍ -풀이&quot;&gt; ‍  풀이&lt;/h2&gt;
&lt;p&gt;이 문제를 풀기 위해서는 크게 두 가지의 로직을 구현해야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;배열을 90도씩 회전시키는 로직&lt;/li&gt;
&lt;li&gt;key를 lock에 겹쳐지는 모든 경우의 수를 순회하고 좌표의 합이 정확히 1이 되는지 확인하는 로직&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;배열-회전&quot;&gt;배열 회전&lt;/h3&gt;
&lt;p&gt;먼저 배열을 90도 회전시키는 로직의 경우 아래와 같이 직접 공식을 계산해서 로직으로 구현했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WJgEh/btslFjjFdTz/MwbhqIsaw3xQgokKtVNURk/img.png&quot; alt=&quot;image-20230627220029427&quot; /&gt;&lt;/p&gt;
&lt;p&gt;위와 같이 되므로 배열의 최대 좌표를 n이라 할 때 (위의 경우 n == 2) 새로운 x 좌표는 기존 y 좌표와 같고 새로운 y 좌표는 (n - 기존 x 좌표)와 같다. 이를 코드로 아래와 같이 구현했다. 주어진 배열 paddingKey를 90도 회전한 새로운 배열을 반환한다.&lt;/p&gt;
&lt;p&gt;코드는 새로운 좌표의 x, y 좌표 자체를 구하는게 아니라 기존에 위치했을 (90도 회전 이전) 위치에 가서 값을 가져오도록 구현했기 때문에 &lt;strong&gt;현재의 x와 y좌표가 이전엔 어디에 있었을까?&lt;/strong&gt; 를 계산해야 하고 따라서 현재 x는 이전의 (n - y)에서, 현재의 y는 이전의 x에서 값을 옮겨오면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public int[][] rotateKey(int M, int[][] paddingKey) {
    int[][] rotatedKey = new int[paddingKey.length][paddingKey.length];

    // M은 배열의 크기로, maxIndex는 좌표의 최대값
    int maxIndex = M - 1;
    for(int i = 0 ; i &amp;lt; M ; i++) {
        for(int j = 0 ; j &amp;lt; M ; j++) {
            // 90도 회전시키는 부분
            rotatedKey[i][j] = paddingKey[maxIndex - j][i];
        }
    }

    return rotatedKey;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key배열을-lock배열에-겹치기&quot;&gt;key배열을 lock배열에 겹치기&lt;/h3&gt;
&lt;p&gt;이제 위에서 만든 배열 회전 메서드를 통해 key 배열을 4방향으로 회전시켜가며 lock에 겹쳐봐야한다. lock에 key가 겹치는 것은 아래와 같은 경우가 모두 포함되어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lXaxk/btslC2CS7WI/uX6fm02HQYJTZ8pjqujBb1/img.png&quot; alt=&quot;image-20230627221453745&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zyIs7/btslC39A9oO/CpiBmMCJ67FlmNFi3CiYA0/img.png&quot; alt=&quot;image-20230627221526605&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bP1mIN/btslA60vO1D/epT4EztOVchOqXaF2P1dRk/img.png&quot; alt=&quot;image-20230627221511388&quot; /&gt;&lt;/p&gt;
&lt;p&gt;따라서, lock과 key를 담을 수 있는 더 큰 배열이 필요하다. lock을 키워도 되고, key를 키워도 되고 다양한 방법이 있겠지만 key를 키우는 방법으로 문제를 풀었다.&lt;/p&gt;
&lt;p&gt;key가 MxM일 때 위 그림처럼 좌상단, 우하단에 한 좌표씩 겹치는 경우까지 모두 포함하려면 패딩된 키 배열의 크기는 M x 2 + (N - 2) 여야 한다. (카카오 해설에선 그냥 M &amp;lt;= N 이므로 lock 배열 NxN을 3배 늘려서 풀었다) &lt;/p&gt;
&lt;p&gt;그렇게 key를 늘려 아래와 같은 상태로 만든다. lock은 개념적으로 key의 한가운데 위치하는 상태이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kIFOT/btslBQXtkiX/uCcQO4ysgrVt6V6E9AeoT1/img.png&quot; alt=&quot;image-20230627222022107&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그리고나서 이제 key를 좌측 상단에서 우측 하단까지 이동시키며 lock과 겹치는 부분의 좌표 합이 각각 1인지, 그리고 겹치지 않는 나머지 lock의 좌표 값이 1인지 확인한다. key 배열은 패딩된 key 배열에서 (0, 0)부터 (N + (M - 1), N + (M - 1)) 까지 이동할 수 있다. 편하게 shift하기 위해 메서드를 만들어 사용했다.&lt;/p&gt;
&lt;p&gt;아래 메서드에서 M까지만 순회하는 이유는 어차피 현재 paddingKey의 좌측 상단 MxM에만 key가 위치해있기 때문이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public int[][] shiftKey(int M, int[][] paddingKey, int startX, int startY) {
    // 매번 새로운 배열을 만들어서 반환
    int[][] shiftedKey = new int[paddingKey.length][paddingKey.length];

    for (int i = 0 ; i &amp;lt; M ; i++) {
        for (int j = 0 ; j &amp;lt; M ; j++) {
            // 이동 작업
            shiftedKey[i + startX][j + startY] = paddingKey[i][j];
        }
    }

    return shiftedKey;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 코드에서는 아래와 같이 순회하면서 shiftKey()를 호출해 shift된 배열을 얻는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;int shiftSize = lock.length + key.length - 1;
for (int startX = 0 ; startX &amp;lt; shiftSize ; startX++) {
    for (int startY = 0 ; startY &amp;lt; shiftSize ; startY++) {
        int[][] shiftedKey = shiftKey(key.length, rotatedKey, startX, startY);
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 위 for-loop 안에서 아래와 같이 lock과의 비교를 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 여기서 lock과의 비교 수행
boolean isOne = true;
for (int x = 0; x &amp;lt; lock.length ; x++) {
    for (int y = 0 ; y &amp;lt; lock.length ; y++) {
        // lock 기준 비교이므로 lock에서의 (x, y)를 shiftKey에서의 좌표로 바꿔주어야 한다.
        int keyX = key.length - 1 + x;
        int keyY = key.length - 1 + y;

        if (lock[x][y] + shiftedKey[keyX][keyY] != 1) {
            isOne = false;
        }
    }
    if (!isOne) {
        // 하나의 좌표라도 합이 1이 아니면 해당 shift는 실패
        break;
    }
}
if (isOne) {
    // 한번이라도 shiftedKey의 모든 좌표 합이 1이 된적이 있으면 자물쇠를 풀 수 있는 것, true 반환
    return true;
}
// 아니면 다음 shiftedKey와 비교 진행
// 모든 경우의 수에서 isOne이 false가 되면 최종 false 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 과정은 하나의 방향에 대해 shift를 수행한 것이므로 위 과정은 각각 다른 방향으로 4번 수행되어야 한다.&lt;/p&gt;
&lt;h3 id=&quot;전체-코드&quot;&gt;전체 코드&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gmelon/algorithm_problems/blob/96b42a2b7f64e79843a2c792643665a1dd3efb9a/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/lv3/60059.%E2%80%85%EC%9E%90%EB%AC%BC%EC%87%A0%EC%99%80%E2%80%85%EC%97%B4%EC%87%A0/%EC%9E%90%EB%AC%BC%EC%87%A0%EC%99%80%E2%80%85%EC%97%B4%EC%87%A0.java&quot;&gt;github 링크&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tech.kakao.com/2019/10/02/kakao-blind-recruitment-2020-round1/&quot;&gt;2020 신입 개발자 블라인드 채용 1차 코딩 테스트 문제 해설&lt;/a&gt;&lt;/p&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/118</guid>
      <comments>https://gmelon.dev/118#entry118comment</comments>
      <pubDate>Tue, 27 Jun 2023 22:30:48 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - 구현] 뱀</title>
      <link>https://gmelon.dev/117</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;☀️-시작&quot;&gt;☀️ 시작&lt;/h2&gt;
&lt;p&gt;풀다 풀다 안 풀려서 결국 답지를 보고 풀었던 문제.. 생각보다 간단했는데 처음에 했던 잘못된 접근법 하나에서 빠져나오지 못해 결국 풀지 못했다. 다음엔 오래 안 풀리는 문제는 며칠 뒤에 다시 도전해보는 것도 재밌을 것 같다.&lt;/p&gt;
&lt;h2 id=&quot; -문제&quot;&gt;  문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/3190&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;먼저 문제는 뱀 게임을 시뮬레이션하는 문제로, 보드 위에서 움직이는 뱀이 벽에 닿거나 자신의 몸과 부딪히면 끝나는 게임의 종료까지 걸리는 시간을 계산하는 문제이다. 입력으로는 사과의 좌표와 방향 전환을 언제, 어떤 방향으로 해야하는지가 주어진다.&lt;/p&gt;
&lt;p&gt;처음에는 문제를 잘못 이해해서 '방향 전환'이라는게 아래와 같이 방향 전환과 직진을 동시에 하는 건줄 알았는데&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5zrHw/btslC3PfFju/8ah34kQKRJDLDuvauoNBDK/img.png&quot; alt=&quot;image-20230627210510740&quot; /&gt;&lt;/p&gt;
&lt;p&gt;문제를 다시 잘 읽어보니 아래와 같이 n초 후에 &lt;strong&gt;뱀의 머리만&lt;/strong&gt; 방향을 전환하는 것이었다. 즉, 4초 후에 기존에 생각했던 3초 후와 같은 상황이 만들어진다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cV1aHp/btslErIYHoI/9W9FTszpTHK54PHDpjBcKk/img.png&quot; alt=&quot;image-20230627210551010&quot; /&gt;&lt;/p&gt;
&lt;p&gt;복잡해서 예제를 직접 따라가보지 않고 문제를 풀었더니 이런 일이 생겼다. 문제를 꼼꼼하게 읽자..!&lt;/p&gt;
&lt;h2 id=&quot; ‍ -풀이&quot;&gt; ‍  풀이&lt;/h2&gt;
&lt;p&gt;사과를 먹으면 뱀의 길이가 늘어나기 떄문에 여러 칸에 걸친 뱀의 움직임을 정확하게 시뮬레이션하는 것이 이 문제 풀이의 핵심이라고 할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;첫번째-방법&quot;&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;/p&gt;
&lt;h3 id=&quot;두번째-방법&quot;&gt;두번째 방법&lt;/h3&gt;
&lt;p&gt;기존엔 좌표가 직접 다음 위치를 계산했다면, 이제는 뱀 전체가 하나가 되어 좌표를 계산하는 방법을 사용했다. 사실 문제에서 이미 방법을 제시하고 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vNfgK/btslEqXBfM8/em08tYsK6w13GxhMeNJkeK/img.png&quot; alt=&quot;image-20230627204355478&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이동 규칙의 3, 4번째를 보면 이동한 칸에 사과가 있다면 꼬리는 움직이지 않고, 사과가 없다면 꼬리가 줄어든다. 이를 그대로 코드에 녹여내면 문제가 풀린다.&lt;/p&gt;
&lt;h4 id=&quot;position-좌표&quot;&gt;Position (좌표)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;static class Position {
    int x, y;

    public Position(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // 주어진 dx, dy만큼 이동한 좌표 반환
    public Position movedPosition(int dx, int dy) {
        return new Position(x + dx, y + dy);
    }

    // 주어진 보드를 벗어났는지 확인
    public boolean isOutOfBound(int N) {
        return x &amp;lt; 1 || x &amp;gt; N || y &amp;lt; 1 || y &amp;gt; N;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Position)) {
            return false;
        }
        Position p = (Position) obj;
        return this.x == p.x &amp;amp;&amp;amp; this.y == p.y;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;방향-정보--뱀-구성&quot;&gt;방향 정보 &amp;amp; 뱀 구성&lt;/h4&gt;
&lt;p&gt;방향의 경우, 뱀이 최초에 동쪽을 바라보고 있으며 (index : 0) 각각 좌측과 우측으로 회전하므로 index가 1 줄어들면 좌측으로 회전하고 1 증가하면 우측으로 회전하는 효과를 내도록 dx, dy 배열을 아래와 같이 구성해주었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 방향 정보, 동쪽부터 시계방향 동/남/서/북
int[] dx = {0, 1, 0, -1};
int[] dy = {1, 0, -1, 0};

// 뱀 몸통
Deque&amp;lt;Position&amp;gt; snake = new LinkedList&amp;lt;&amp;gt;();

// 초기 위치, 방향 설정
int currentDirectionIndex = 0; // 0(동), 1(남), 2(서), 3(북)
snake.offer(new Position(1, 1));&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;꼬리-이동을-결정하는-코드&quot;&gt;꼬리 이동을 결정하는 코드&lt;/h4&gt;
&lt;p&gt;아래 코드에서 &lt;code&gt;apples&lt;/code&gt; 는 각각 사과의 위치를 담고있는 List&amp;lt;Position\&amp;gt; 이다. 또한, &lt;code&gt;snake&lt;/code&gt; Deque가 현재 몸통의 좌표 Position들을 가지고 있으므로 contains() 메서드로 새로운 머리가 몸통과 부딪히는지를 판단할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 현재 뱀의 머리를 가져와 현재 방향대로 한 칸 이동
Position nextHead = snake.peekFirst().movedPosition(dx[currentDirectionIndex], dy[currentDirectionIndex]);

if (nextHead.isOutOfBound(N) || snake.contains(nextHead)) {
    // 벽이나 몸통과 부딪히는 경우
    break;
} else {
    // 부딪히지 않는 경우 
    snake.offerFirst(nextHead); // 먼저 머리 늘리기
    if (!apples.contains(nextHead)) {
        // 새로운 머리에 사과가 없으면 꼬리 하나 제거
        snake.pollLast();
    } else {
        // 사과가 있으면 사과 제거
        apples.remove(nextHead);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드처럼 무조건 새로운 좌표를 뱀의 머리에 추가하고(offerFirst()), 사과가 있다면 사과를 제거하고 사과가 없다면 꼬리를 제거해준다(pollLast()). 이렇게 하면 뱀 몸통의 움직임을 하나하나 계산해줄 필요 없이 뱀이 움직이고 증가하는대로 몸통이 속한 좌표를 정확히 유지할 수 있다.&lt;/p&gt;
&lt;p&gt;그리고 마지막으로 이동을 완료한 후에 현재 시간에 방향 전환 지시가 있으면 방향을 전환해주면 된다. 문제에서 방향 전환은 'n초 후'에 이뤄진다고 했으므로 이동이 끝난 후에 뱀의 머리 방향을 틀어준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 이동 완료 후 방향 전환이 있으면 수행
if (!rotations.isEmpty() &amp;amp;&amp;amp; rotations.peek().time == time) {
    currentDirectionIndex = rotations.poll().nextDirectionIndex(currentDirectionIndex);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;새로운 방향을 얻는 코드는 방향 전환에 대한 정보를 담는 Rotation 클래스에 작성해주었다.&lt;/p&gt;
&lt;h4 id=&quot;rotation&quot;&gt;Rotation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;static class Rotation {
    int time;
    char direction;

    public Rotation(int time, char direction) {
        this.time = time;
        this.direction = direction;
    }

    // Rotation이 갖고 있는 방향 전환 정보 direction('L' or 'D') 에 따라
    // 다음 directionIndex를 계산한다
    public int nextDirectionIndex(int currentDirectionIndex) {
        if (direction == 'L') {
            currentDirectionIndex--;
            if (currentDirectionIndex == -1) {
                return 3;
            }
        } else {
            currentDirectionIndex++;
            if (currentDirectionIndex == 4) {
                return 0;
            }
        }
        return currentDirectionIndex;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;뱀이 벽에 부딪히거나 몸통에 닿아 시뮬레이션이 끝나면 끝난 시점의 시간을 출력하면 된다. 몇 초 후에 게임이 종료되는지를 반환하는 것이므로 예를 들어 5초까지 정상 상태이다가 6초째 이동에서 게임이 종료되었다면 6초를 반환해주어야 하므로 &lt;code&gt;time&lt;/code&gt; 변수는 이동이나 조건 검사 이전에 증가시켜주도록 작성했다.&lt;/p&gt;
&lt;h3 id=&quot;전체-코드&quot;&gt;전체 코드&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gmelon/algorithm_problems/blob/c0df62599c8912461bbe27405aa25f504dca6a58/%EB%B0%B1%EC%A4%80/Gold/3190.%E2%80%85%EB%B1%80/%EB%B1%80.java&quot;&gt;github 링크&lt;/a&gt;&lt;/p&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/117</guid>
      <comments>https://gmelon.dev/117#entry117comment</comments>
      <pubDate>Tue, 27 Jun 2023 21:07:01 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - 구현] 기둥과 보 설치</title>
      <link>https://gmelon.dev/116</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;☀️-시작&quot;&gt;☀️ 시작&lt;/h2&gt;
&lt;p&gt;이번 문제는 2020 신입 개발자 블라인드 채용 1차 코딩 테스트의 5번 문항 '기둥과 보 설치' 문제이다. 확실히 구현문제는 어렵지만 머릿속으로 풀이 과정을 생각하고 이를 코드로 구현해내는 과정이 재밌고 뿌듯하다.&lt;/p&gt;
&lt;p&gt;정답률이 1.9%라는데 나도 그랬지만 카카오 코테는 블라인드라 일단 지원하고 보는 사람이 너무 많아서 정답률에 크게 의미를 두지 않아야 할 것 같다는 생각이 든다. 그래도 항상 손도 못대던 카카오 코테인데 이번엔 직접 풀 수 있어서 기분이 좋았다.&lt;/p&gt;
&lt;h2 id=&quot; -문제&quot;&gt;  문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/60061&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;먼저 문제의 구현 요구사항을 간단히 요약하면, 죠르디가 '기둥'과 '보'를 사용해 구조물을 만들려고 하는데 입력으로 주어지는 '설치'와 '삭제' 요청을 시뮬레이션하면서 가능한 요청만 반영하여 최종 완성된 구조물을 출력하면 되는 문제이다.&lt;/p&gt;
&lt;p&gt;'기둥'과 '보'의 설치 조건은 각각 아래와 같다. (기둥과 보는 모두 길이가 1인 선분이며 보는 주어진 좌표에서 '우측'으로, 기둥은 주어진 좌표에서 '위'로 설치된다)&lt;/p&gt;
&lt;h4 id=&quot;기둥-설치-조건&quot;&gt;기둥 설치 조건&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;바닥(y좌표가 0)위에 설치 가능&lt;/li&gt;
&lt;li&gt;보의 한쪽 끝에 위치하면 설치 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;보-설치-조건&quot;&gt;보 설치 조건&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;양쪽 끝 중 하나가 기둥 위에 있으면 설치 가능&lt;/li&gt;
&lt;li&gt;양쪽 끝 부분이 각각 다른 보와 연결되면 설치 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;또한, 입력은 2차원 배열로 주어지는데 각 행 &lt;code&gt;[x, y, a, b]&lt;/code&gt;는 각각 &lt;code&gt;[가로좌표, 세로좌표, 설치할 구조물 종류, 삭제 or 설치]&lt;/code&gt;를 의미하며 기둥(0), 보(1), 삭졔(0), 설치(1)로 표현된다.&lt;/p&gt;
&lt;h2 id=&quot; ‍ -풀이&quot;&gt; ‍  풀이&lt;/h2&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;h3 id=&quot;설치-조건-검사&quot;&gt;설치 조건 검사&lt;/h3&gt;
&lt;p&gt;이 중 기둥이나 보를 설치하는 경우에는 문제에서 제시된 조건을 토대로 설치가 가능한지 판단한다. 구체적인 방법으로는 먼저 x, y 좌표를 갖는 Position 클래스를 만들고 기둥과 보의 리스트를 각각 만들어둔다.&lt;/p&gt;
&lt;h4 id=&quot;position-클래스-list에-넣을-것이므로-equals-구현&quot;&gt;Position 클래스 (List에 넣을 것이므로 equals() 구현)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;static class Position{
    int x, y;
    public Position(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // 주어진 dx, dy를 더해 새로운 Position 반환
    // 조건 검사 시 주어진 좌표에서 원하는 좌표를 쉽게 만들어내기 위해 사용
    public Position adjustedNewPosition(int dx, int dy) {
        return new Position(x + dx, y + dy);
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (!(o instanceof Position)) {
            return false;
        }
        Position p = (Position) o;
        return this.x == p.x &amp;amp;&amp;amp; this.y == p.y;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;기둥과-보의-리스트&quot;&gt;기둥과 보의 리스트&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;List&amp;lt;Position&amp;gt; columns = new ArrayList&amp;lt;&amp;gt;(); // 기둥(0)
List&amp;lt;Position&amp;gt; rows = new ArrayList&amp;lt;&amp;gt;(); // 보(1)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 입력이 들어올 때 마다 순회하면서 새로운 좌표가 기존 기둥, 보들 사이에 설치 가능한지 (문제에서 주어진 조건을 토대로) 검사한다.&lt;/p&gt;
&lt;h4 id=&quot;기둥-설치-검사-메서드&quot;&gt;기둥 설치 검사 메서드&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public boolean canBuildColumn(List&amp;lt;Position&amp;gt; columns, List&amp;lt;Position&amp;gt; rows, Position newPosition) {
    // 주어진 좌표가 바닥인가?
    if (newPosition.y == 0) {
        return true;
    }

    // 주어진 좌표 &quot;아래&quot;에 기둥이 있는가?
    if (columns.contains(newPosition.adjustedNewPosition(0, -1))) {
        return true;
    }

    // 해당 좌표 || &quot;좌측&quot; 좌표에 보가 있는가?
    if (rows.contains(newPosition) || rows.contains(newPosition.adjustedNewPosition(-1, 0))) {
        return true;
    }

    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;보-설치-검사-메서드&quot;&gt;보 설치 검사 메서드&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public boolean canBuildRow(List&amp;lt;Position&amp;gt; columns, List&amp;lt;Position&amp;gt; rows, Position newPosition) {
    // 주어진 좌표 아래&quot;나&quot; 주어진 좌표 우측 아래에 기둥이 있는가?
    if (columns.contains(newPosition.adjustedNewPosition(0, -1)) ||
       columns.contains(newPosition.adjustedNewPosition(1, -1))) {
        return true;
    }

    // 현재 좌표의 좌측 좌표&quot;와&quot; 우측 좌표에 보가 있는가?
    if (rows.contains(newPosition.adjustedNewPosition(-1, 0)) &amp;amp;&amp;amp;
       rows.contains(newPosition.adjustedNewPosition(1, 0))) {
        return true;
    }

    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 사용해 설치가 가능한지 판단하게 된다.&lt;/p&gt;
&lt;h4 id=&quot;위-메서드를-실제로-사용하는-코드&quot;&gt;위 메서드를 실제로 사용하는 코드&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 시뮬레이션 시작
for (int[] build : build_frame){
    Position newPosition = new Position(build[0], build[1]); // 가로, 세로
    if (build[3] == 1) { // 설치
        if (build[2] == 0) {
            // 기둥 설치 조건 검사
            if(canBuildColumn(columns, rows, newPosition)) {
                columns.add(newPosition);
            }

        } else {
            // 보 설치 조건 검사
            if (canBuildRow(columns, rows, newPosition)) {
                rows.add(newPosition);
            }
        }
    }
    // 이후에는 삭제하는 경우에 대한 처리 진행&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;삭제-조건-검사&quot;&gt;삭제 조건 검사&lt;/h3&gt;
&lt;p&gt;이제 반대로 삭제하는 경우를 생각해보자. 처음에는 설치 시와 마찬가지로 직접 삭제가 가능한 조건을 따져보고 있었다. 예를 들면 기둥을 삭제할 수 있는 조건은 주어진 좌표 위 좌표에 기둥이 없어야 하고, 주어진 좌표의 위좌표와 위+좌측 좌표에 보가 없어야 하고… 이를 따지다보니 너무 복잡하고 실수할 확률이 높아보여서 방법을 바꾸었다.&lt;/p&gt;
&lt;p&gt;새로운 방법은 삭제 요청이 들어올 때 기존 기둥/보 리스트에서 임시로 새로운 좌표를 삭제한 뒤 나머지 기둥/보가 그대로 존재할 수 있는지를 확인해보는 방법이다. 그리고 기존 기둥/보 중 하나라도 그 자리에 유지되지 못한다면 다시 해당 좌표를 원복시킨다. 즉, 아래와 같이 코드로 작성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 상단은 설치 요청에 대한 처리 코드

else { // 삭제
    boolean canRemove = true;

    if (build[2] == 0) {
        // 임시로 기둥 삭제
        columns.remove(newPosition);

        // 기존 기둥, 보가 유지 가능한지 검사
        for(Position column: columns) {
            canRemove = canBuildColumn(columns, rows, column);
            if (!canRemove) {
                break;
            }
        }
        if (canRemove) {
            // 최적화를 위함
            for(Position row: rows) {
                canRemove = canBuildRow(columns, rows, row);
                if (!canRemove) {
                    break;
                }
            }                        
        }

        // 삭제 불가능하면 다시 추가
        if (!canRemove) {
            columns.add(newPosition);
        }
    } else {
        // 임시로 보 삭제
        rows.remove(newPosition);

        // 기존 기둥, 보가 유지 가능한지 검사
        for(Position column: columns) {
            canRemove = canBuildColumn(columns, rows, column);
            if (!canRemove) {
                break;
            }
        }
        if (canRemove) {
            for(Position row: rows) {
                canRemove = canBuildRow(columns, rows, row);
                if (!canRemove) {
                    break;
                }
            }
        }

        // 삭제 불가능하면 다시 추가
        if (!canRemove) {
            rows.add(newPosition);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 처럼 기존에 설치 요청 검증을 위해 만들어 두었던 메서드에 현재 삭제 요청이 반영된 리스트를 넣고 해당 리스트의 원소를 순회하면서 삭제 요청이 반영되어도 그 자리에 계속 있을 수 있는지를 확인한다. 이렇게 하면 삭제 요청은 시간복잡도 O(N^2)가 되지만 어차피 입력은 1000개 안쪽으로 주어지므로 이렇게 구현해도 문제가 되지 않을 것 같았다. (카카오 코테 때 이 문제에 효율성 채점을 하지 않긴 했다)&lt;/p&gt;
&lt;h3 id=&quot;정답-배열-구성&quot;&gt;정답 배열 구성&lt;/h3&gt;
&lt;p&gt;이렇게 순회를 마치면 리스트 columns, rows에 모든 요청을 처리하고 마지막 상태에 남아있는 기둥과 보가 담겨있게 된다. 문제에서 이를 &lt;code&gt;[가로좌표, 세로좌표, 코드](코드 - 기둥(0), 보(1))&lt;/code&gt; 을 행으로 갖는 2차원 배열 형태(앞의 원소부터 오름차순 정렬)로 반환하라고 했으므로 자바의 Stream을 사용해 2차원 배열을 만들어준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;return Stream.concat(
    columns.stream().map(column -&amp;gt; new int[]{column.x, column.y, 0}),
    rows.stream().map(row -&amp;gt; new int[]{row.x, row.y, 1}))
    .sorted(Comparator.comparingInt((int[] arr) -&amp;gt; arr[0])
           .thenComparingInt(arr -&amp;gt; arr[1])
           .thenComparingInt(arr -&amp;gt; arr[2]))
    .toArray(int[][]::new);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게까지 스트림을 써본건 처음인데, 위 코드를 원래는 아래와 같이 풀어냈었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 배열로 만들어서 반환
int answerSize = columns.size() + rows.size();
int[][] answer = new int[answerSize][3];

int index = 0;
for(Position column: columns) {
    answer[index][0] = column.x;
    answer[index][1] = column.y;
    answer[index][2] = 0; // 기둥
    index++;
}
for(Position row: rows) {
    answer[index][0] = row.x;
    answer[index][1] = row.y;
    answer[index][2] = 1; // 보
    index++;
}

// 정렬 후 반환
Arrays.sort(answer, 
            Comparator.comparingInt((int[] arr) -&amp;gt; arr[0])
           .thenComparingInt(arr -&amp;gt; arr[1])
           .thenComparingInt(arr -&amp;gt; arr[2]));

return answer;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 문제를 풀고 나서 찾아보니 &lt;code&gt;Stream.concat()&lt;/code&gt;을 써서 각 리스트를 1차원 배열의 Stream으로 만들고 합쳐서 해결할 수 있는 방법이 있었다. 확실히 스트림을 쓰는게 가독성과 (코드 작성 시의) 효율성면에서 엄청난 이점이 있는 것 같다. 내가 모르는 Stream 함수를 좀 더 찾아보고 써봐야겠다는 생각을 하게 됐다.&lt;/p&gt;
&lt;h3 id=&quot;전체-코드&quot;&gt;전체 코드&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gmelon/algorithm_problems/blob/9859c94f4cc5b1b5642e50db2198bcd2dbb9df33/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/lv3/60061.%E2%80%85%EA%B8%B0%EB%91%A5%EA%B3%BC%E2%80%85%EB%B3%B4%E2%80%85%EC%84%A4%EC%B9%98/%EA%B8%B0%EB%91%A5%EA%B3%BC%E2%80%85%EB%B3%B4%E2%80%85%EC%84%A4%EC%B9%98.java&quot;&gt;github 링크&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tech.kakao.com/2019/10/02/kakao-blind-recruitment-2020-round1/&quot;&gt;2020 신입 개발자 블라인드 채용 1차 코딩 테스트 문제 해설&lt;/a&gt;&lt;/p&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/116</guid>
      <comments>https://gmelon.dev/116#entry116comment</comments>
      <pubDate>Tue, 27 Jun 2023 16:04:07 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] Item 23 - 태그 달린 클래스보다는 클래스 계층구조를 활용하라</title>
      <link>https://gmelon.dev/115</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;태그-달린-클래스&quot;&gt;태그 달린 클래스&lt;/h2&gt;
&lt;p&gt;아래와 같이 내부적으로 특정 필드 (태그 역할)을 갖고 이 필드에 값에 따라 다른 동작을 수행하는 클래스를 책에선 '태그 달린 클래스'라고 표현하고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class Figure {
    public enum Shape {RECTANGLE, CIRCLE};
    private final Shape shape; // 태그

    // RECTANGLE용 필드
    private double length;
    private double width;

    // CIRCLE용 필드
    private double radius;

    // RECTANGLE용 생성자
    public Figure(double length, width) {
        ...
    }

    // CIRCLE용 생성자
    public Figure(double radius) {
        ...
    }

    // Shape에 따라 로직이 달라지는 메서드
    public double area() {
        switch(shape) {
            case A:
                ...
                return ...;
            case B:
                ...
                return ...;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 식으로 내부 필드에 따라 서로 다른 분기를 타는 클래스를 말하는데 물론 이러한 클래스는 단점이 매우 많다. 먼저 가장 눈에 띄는 것은 switch를 사용한 분기 코드이다.&lt;/p&gt;
&lt;p&gt;만약 새로운 의미를 추가하려면 클래스에서 Shape를 사용하는 모든 코드를 찾아 코드를 추가해주어야 한다. 현재는 area() 메서드만 Shape에 따른 분기를 사용하고 있지만 이러한 코드가 많을 경우 이를 모두 찾아 새로운 의미에 대한 코드를 추가해주어야 한다. 이 과정에서 누락되거나 실수를 해서 오류가 발생할 가능성이 높다.&lt;/p&gt;
&lt;p&gt;또한 각 의미 별로 필요한 필드가 다르기 때문에 하나의 클래스에 모든 의미에 해당하는 필드와 생성자가 위치하게 되어 코드가 불필요하게 복잡해진다.&lt;/p&gt;
&lt;h2 id=&quot;클래스-계층구조&quot;&gt;클래스 계층구조&lt;/h2&gt;
&lt;p&gt;그럼 이를 클래스 계층 구조로 전환해보자. 방법은 아래와 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;계층 구조의 루트가 될 &lt;strong&gt;추상 클래스&lt;/strong&gt;를 정의&lt;/li&gt;
&lt;li&gt;태그 값에 따라 달라지는 메서드는 추상 클래스의 &lt;strong&gt;추상 메서드&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;태그와 관계없는 메서드/필드는 추상 클래드에 &lt;strong&gt;일반 메서드/필드&lt;/strong&gt;로 둠&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;따라서 위 예시의 경우 아래와 같이 분리할 수 있다.&lt;/p&gt;
&lt;h4 id=&quot;figure-추상-클래스&quot;&gt;Figure 추상 클래스&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public abstract class Figure {
    // 태그 값에 따라 동작이 달라지는 메서드
    abstract double area();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;circle-extends-figure&quot;&gt;Circle extends Figure&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public Circle extends Figure {
    private final double radius;

    public Circle(...) {
        ...
    }

    // 각자의 의미에 맞게 추상 메서드 구현    
    @Override
    public double area {
        return ...;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;rectangle-extends-figure&quot;&gt;Rectangle extends Figure&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public Rectangle extends Figure {
    private final double length;
    private final double width;

    public Rectangle(...) {
        ...
    }

    // 각자의 의미에 맞게 추상 메서드 구현
    @Override
    public double area {
        return ...;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 식으로 계층 구조를 활용하면 의미별 타입이 별도로 존재하게 되어 특정 의미만 매개변수로 받도록 제한할 수 있게 된다. 예를 들어 Rectangle 하위에 정사각형 Square 클래스가 있을 때 어떤 메서드가 아래와 같다면 Circle이 아닌 Rectangle 하위 계층의 클래스들만 지원한다고 명시할 수 있게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public void someMethod(Rectangle rectangle) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또, 결국 모두 루트 클래스인 Figure을 상속받기 때문에 동일한 타입으로 사용할 수 있는 등 유연성을 제공한다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;이펙티브 자바 &lt;a href=&quot;https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94&quot;&gt;전체 아이템 목록&lt;/a&gt; (스터디 정리 레포지토리)&lt;/p&gt;
&lt;/blockquote&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/115</guid>
      <comments>https://gmelon.dev/115#entry115comment</comments>
      <pubDate>Tue, 20 Jun 2023 21:39:35 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] Item 22 - 인터페이스는 타입을 정의하는 용도로만 사용하라</title>
      <link>https://gmelon.dev/114</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;인터페이스는 &lt;strong&gt;자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입&lt;/strong&gt;의 역할을 수행한다. Item 22는 인터페이스를 오직 이 용도로만 사용하라고 조언한다.&lt;/p&gt;
&lt;p&gt;예를 들어 아래와 같이 아무런 메서드가 선언되어 있지 않은 인터페이스에 상수만 선언해 사용하면 안 된다. (상수 인터페이스 패턴)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public interface SomeConstants {
    int VALUE_ONE = 0;
    int VALUE_TWO = 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이러한 용도로 인터페이스를 사용하게 되면 &lt;strong&gt;내부 구현&lt;/strong&gt;이 외부로 그대로 노출되는 것이므로 (인터페이스의 상수는 무조건 public static final) 지양해야 한다. 인터페이스의 목적에 맞지 않는 사용법이라고 할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;또한 상수를 편하게 사용하기 위해 클라이언트가 인터페이스를 구현하게 되면 상수 인터페이스에 선언된 상수 이름과 구현한 클래스의 네임 스페이스가 섞여 혼란을 주는 문제가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class MyClass implements SomeConstants {
    public void someMethod() {
        int a = VALUE_ONE; // 인터페이스의 상수
        int VALUE_ONE = 1; // 지역 변수
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;상수를-공개하는-다른-방법&quot;&gt;상수를 공개하는 다른 방법&lt;/h2&gt;
&lt;p&gt;그럼 의도적으로 상수를 공개하기 위해서는 어떻게 하면 될까?&lt;/p&gt;
&lt;p&gt;먼저 특정 클래스, 인터페이스와 강한 연관이 있는 상수라면 해당 클래스, 인터페이스 자체에 상수로 두는 방법이 있다. Integer.MIN_VALUE, Integer.MAX_VALUE가 이에 해당하는 예시이다.&lt;/p&gt;
&lt;p&gt;또한, 서로 연관이 있는 여러 개의 상수가 존재하고 상수별로 특정 값을 갖거나 특정 의미를 갖는 경우 열거형을 사용해 표현하는 방법도 고려할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public enum Day {
    MONDAY(0),
    TUESDAY(1),
    WEDNESDAY(2),
    THURSDAY(3),
    FRIDAY(4),
    SATURDAY(5),
    SUNDAY(6);

    private final int value;

    Day(int value) {
        this.value = value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 이렇게 하는 것도 불가능한 경우라면 별도의 클래스를 만들어서 상수를 모아두되, 인스턴스화가 불가능한 클래스로 만들어서 사용하는 방법도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class SomeConstants {
    private SomeConstants() {
        // 인스턴스화, 상속 방지
    }
    public static final int VALUE_ONE = 0;
    public static final int VALUE_TWO = 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;인터페이스가 아닌 인스턴스화 불가능한 클래스를 통해 상수를 공개하게 되면 구현을 위해 존재하는 인터페이스와 다르게 상수용 클래스라는 목적을 분명히 밝히고 상속 또한 금지시키면서 부작용을 최소화하며 상수를 공개할 수 있을 것 같다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;이펙티브 자바 &lt;a href=&quot;https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94&quot;&gt;전체 아이템 목록&lt;/a&gt; (스터디 정리 레포지토리)&lt;/p&gt;
&lt;/blockquote&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/114</guid>
      <comments>https://gmelon.dev/114#entry114comment</comments>
      <pubDate>Tue, 20 Jun 2023 20:41:01 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] Item 19 - 상속을 고려해 설계하고 문서화하라 그러지 않았다면 상속을 금지하라</title>
      <link>https://gmelon.dev/113</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;상속을-위한-문서화&quot;&gt;상속을 위한 문서화&lt;/h2&gt;
&lt;p&gt;상속을 위한 문서화란 상속이 가능한 클래스의 재정의 가능 메서드에 해당 메서드를 내부적으로 어떻게 이용하고 있는지, 그래서 어떤 식으로 동작하도록 구현되어야 하는지 문서로 남겨두는 것을 말한다.&lt;/p&gt;
&lt;p&gt;이렇게 해야 하는 이유는 클래스를 상속받아 구현된 클래스에서 해당 메서드를 부모 클래스에서의 의도와 다르게 구현할 경우 의도치 않은 동작으로 이어질 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;자바 API에서는 이러한 문서를 &lt;code&gt;Implementation Requirements&lt;/code&gt; (코드에선 &lt;code&gt;@ImplSpec&lt;/code&gt;)라는 항목으로 문서화하여 제공하고 있다. 아래는 AbstractCollection.remove() 메서드 일부이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5y3OF/btskJGHyOdA/AHAtB604G6C4hjBVOC4p21/img.png&quot; alt=&quot;image-20230620184640552&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이를 통해 Abstract iterator()가 반환하는 Iterator의 remove() 동작을 임의로 재정의하게 되면 컬렉션에서 원소를 삭제하는 기능에 문제가 발생할 수도 있음을 알 수 있다. 이런 식으로 내부 동작을 문서화하여 남김으로써 클래스를 상속해서 메서드를 재정의할 때 개발자가 어떻게 구현해야 하는지 인지하도록 해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;상속은-캡슐화를-해친다&quot;&gt;상속은 캡슐화를 해친다&lt;/h3&gt;
&lt;p&gt;하지만 API는 말그대로 인터페이스이기 때문에 좋은 API문서는 '어떻게' 가 아닌 '무엇'에 초점을 맞추어 설명해야 한다. 이는 상속이 캡슐화를 해치기 때문에 발생하는 현상이다.&lt;/p&gt;
&lt;p&gt;즉, 상속이 가능하도록 클래스를 설계함으로써 내부 구현이 (문서를 통해) 외부에 노출되는 일이 벌어지게 되는 것이다.&lt;/p&gt;
&lt;h2 id=&quot;상속용-클래스-설계-시-유의사항&quot;&gt;상속용 클래스 설계 시 유의사항&lt;/h2&gt;
&lt;h3 id=&quot;hook-메서드&quot;&gt;hook 메서드&lt;/h3&gt;
&lt;p&gt;효율적인 상속용 클래스 설계를 위해서는 내부 로직 중 일부를 분리해서 protected 메서드로 공개해야 할 수도 있다고 한다. 예를 들어 AbstractList.removeRange() 의 경우 protected 메서드 인데 컬렉션(특히 부분리스트)의 clear() 메서드가 이 removeRange() 메서드를 사용해 원소를 제거하므로 구현 클래스에서 &lt;strong&gt;각 컬렉션의 특징에 맞게&lt;/strong&gt; removeRange() 메서드를 재정의하여 효율성을 높일 수 있도록 했다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sZvn6/btskJVkjlnQ/Z1J5ecreGD5OnrFq7gZLQ0/img.png&quot; alt=&quot;image-20230620185459668&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHc4FI/btskJimMuyC/KmjiXSK3knD1TODXShbF71/img.png&quot; alt=&quot;image-20230620185507649&quot; /&gt;&lt;/p&gt;
&lt;p&gt;예를 들어 ArrayList에서는 배열이라는 특성을 이용해 아래와 같이 removeRange를 재정의하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x3Hya/btskJiNQv5L/TcBokCWn3OxEU1kGryHeW1/img.png&quot; alt=&quot;image-20230620185559332&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그리고 ArrayList의 clear()에서는 removeRange()를 호출하지 않지만 ArrayList.SubList.clear() 에서는 부모인 AbstractList.clear()을 그대로 호출하기 때문에 ArrayList에서 (컬렉션에 특성에 맞게) 재정의한 removeRange()를 호출하는 SubList.removeRange()가 clear() 동작에 사용된다.&lt;/p&gt;
&lt;h4 id=&quot;arraylistsublistremoverange&quot;&gt;ArrayList.SubList.removeRange()&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beNjkE/btskKmhLycx/o2L39oSwhe3uNcEVPjSll1/img.png&quot; alt=&quot;image-20230620185718000&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;생성자에서-재정의-가능-메서드-호출-금지&quot;&gt;생성자에서 재정의 가능 메서드 호출 금지&lt;/h3&gt;
&lt;p&gt;Item 13 의 clone() 에서도 &lt;a href=&quot;https://sh-hyun.tistory.com/110#-clone-%EC%97%90%EC%84%9C%EB%8A%94-%EC%9E%AC%EC%A0%95%EC%9D%98-%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%A9%94%EC%84%9C%EB%93%9C-%ED%98%B8%EC%B6%9C-%EA%B8%88%EC%A7%80&quot;&gt;비슷한 이야기&lt;/a&gt;가 있었는데, 생성자에서도 재정의 가능 메서드를 호출하면 안 된다. 상속 클래스의 생성자는 계속해서 부모의 생성자를 super()로 호출하게 되는데 이 과정에서 부모 클래스 레벨의 생성자가 하위 클래스의 &lt;strong&gt;재정의된&lt;/strong&gt; 메서드를 호출해버리는 문제가 발생할 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;현재 물리적으로 부모 클래스의 코드가 실행되고 있더라도 실제 인스턴스는 하위 클래스이기 때문에 재정의된 메서드를 호출하면 하위 클래스의 메서드가 호출되고 이는 비정상적인 동작을 불러일으킨다.&lt;/p&gt;
&lt;p&gt;따라서, 생성자에서는 절대 재정의 가능 메서드를 호출해선 안 되고 필요한 메서드는 private로 분리해서 호출하도록 해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;cloneable--serializable-구현-클래스의-상속&quot;&gt;Cloneable / Serializable 구현 클래스의 상속&lt;/h3&gt;
&lt;p&gt;Cloneable 이나 Serializable 인터페이스를 구현하는 클래스를 상속 가능하도록 만들면 이를 상속받는 하위의 모든 클래스들이 복제와 직렬화 기능을 고려하여 구현되어야 하기 때문에 구현이 복잡해지고 어려워진다. 따라서 이들 인터페이스를 구현하는 클래스는 상속을 막는 것이 더 적절하다고 한다.&lt;/p&gt;
&lt;h2 id=&quot;상속을-금지하라&quot;&gt;상속을 금지하라&lt;/h2&gt;
&lt;p&gt;지금까지 코드를 작성하면서 클래스에 final 키워드를 달아본 적은 거의 없었던 것 같다. 책에서는 상속용으로 설계하지 않은 클래스는 안전하게 모두 상속을 막아버리라고 조언한다.&lt;/p&gt;
&lt;p&gt;이를 위한 방법으로 final로 클래스를 선언하거나 생성자를 private / default 로 선언할 수 있다. 후자의 경우 앞서 말했듯 상속 클래스는 부모 클래스의 생성자를 호출해야 하므로 이게 막혀버리면 상속 자체가 불가능해진다. 이 경우 정적 팩토리 메서드를 통해 객체를 생성하는 API를 제공할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 것이다. 상속 대신 컴포지션을 우선해서 사용하도록 설계하라!!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
  &lt;p&gt;이펙티브 자바 &lt;a href=&quot;https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94&quot;&gt;전체 아이템 목록&lt;/a&gt; (스터디 정리 레포지토리)&lt;/p&gt;
&lt;/blockquote&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/113</guid>
      <comments>https://gmelon.dev/113#entry113comment</comments>
      <pubDate>Tue, 20 Jun 2023 19:08:30 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] Item 13 - clone 재정의는 주의해서 진행하라</title>
      <link>https://gmelon.dev/110</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;아이템 13에서는 객체의 복제를 위해 사용하는 &lt;code&gt;Object.clone()&lt;/code&gt; 메서드를 제대로 재정의하기 위해서는 어떻게 해야 하는지에 대해 설명하고 있다. 또한, &lt;code&gt;Object.clone()&lt;/code&gt; 이 동작하도록 하기 위해 구현해야 하는 Cloneable 인터페이스의 문제점에 대해 이야기하고 결론적으로는 새로운 인터페이스/클래스는 절대 Cloneable을 확장/구현하면 안 되고, 객체의 복제 기능을 구현하기 위해서는 &lt;code&gt;생성자와 팩토리&lt;/code&gt;를 사용해야 한다는 조언을 하고 있다.&lt;/p&gt;
&lt;h2 id=&quot;objectclone과-cloneable의-동작-방식&quot;&gt;Object.clone()과 Cloneable의 동작 방식&lt;/h2&gt;
&lt;p&gt;먼저 이 메서드의 동작 방식에 대해 알아보자. 일반적인 경우와 달리 이 메서드는 선언은 Object 클래스에 되어있지만 빈 인터페이스인 Cloneable을 구현해야만 제대로 작동하도록 설계되어 있다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;code&gt;CloneNotSupportedException&lt;/code&gt; – if the object's class does not support the Cloneable interface. Subclasses that override the clone method can also throw this exception to indicate that an instance cannot be cloned.&lt;/p&gt;
  &lt;p&gt;Object.clone() API 문서, Cloneable을 구현하지 않은 클래스가 clone()을 호출하면 CloneNotSupportedException이 터진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 메서드가 없는 빈 인터페이스인 Cloneable
public interface Cloneable {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또한, Object.clone()은 protected로 선언되어 하위 클래스에서 재정의해주지 않으면 클라이언트에서 호출할 수 없게 되어있다. 이러한 방식은 상위 클래스에서 정의한 clone()을 동작을 인터페이스가 변경하게 되므로 상당히 이례적인 설계라고 할 수 있지만 실무에서 Cloneable을 구현한 클래스가 흔하게 사용되므로 사용법에 대해서는 알아두는게 좋다고 말한다.&lt;/p&gt;
&lt;h2 id=&quot;올바른-clone-구현-방법&quot;&gt;올바른 clone() 구현 방법&lt;/h2&gt;
&lt;h3 id=&quot;가변-객체를-참조하지-않는-객체&quot;&gt;가변 객체를 참조하지 않는 객체&lt;/h3&gt;
&lt;p&gt;먼저 &lt;code&gt;모든 필드가 기본 타입이거나 불변 객체를 참조하는 객체&lt;/code&gt; 의 경우 어떻게 clone()을 구현해야 되는지 살펴보자.&lt;/p&gt;
&lt;p&gt;이런 경우 먼저 super.clone()을 호출한다. 그럼 그렇게 만들어진 객체가 원본과 똑같은 값을 같는 복제본이 된다. 모든 필드가 기본 타입이거나 불변 객체이므로 이렇게 만들어진 복제본에서 더 이상 수정할 것이 없다. 따라서 그대로 반환해도 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class Number implements Cloneable {
    private final int value; // 기본 타입

    // 생성자 생략

    @Override
    public Number clone() {
        try {
            return (Number) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // 도달 불가능
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 봐야할 것은 먼저 Object.clone()은 Object를 반환하지만 자바에서는 재정의 시 기존 반환 타입의 하위 타입 반환이 가능하므로 Number를 반환하도록 할 수 있다. 이렇게하면 클라이언트에서 일일이 형변환을 해줄 필요가 없어진다. 또한, Number 클래스가 Cloneable을 구현하고 다른 클래스를 상속받지 않으니 &lt;code&gt;CloneNotSupportedException&lt;/code&gt;이 터질 일이 없으므로 불필요한 checked exception을 try-catch로 감싸서 메서드의 throws 절을 없애고 클라이언트에서 더 편하게 사용할 수 있도록 해주면 좋다고 한다.&lt;/p&gt;
&lt;h3 id=&quot;가변-객체를-참조하는-객체&quot;&gt;가변 객체를 참조하는 객체&lt;/h3&gt;
&lt;p&gt;만약 참조하는 필드에 가변 객체가 포함된다면 상황이 좀 복잡해진다. 단순히 위에서 했던 것처럼 clone()을 구현하면 어떻게 될까?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class Stack {
    private Object[] elements;

    // 생성자 생략

    // 잘못된 구현
    @Override
    public Stack clone() {
        try {
            return (Stack) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // 도달 불가능
        }
    }

    // 이후 메서드 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 clone()을 구현하면 복제된 객체와 원본 객체가 &lt;code&gt;동일한 주소의 elements 배열을 참조&lt;/code&gt;하는 문제가 생긴다. Object.clone()은 단순히 객체의 주소를 복사해서 새로운 객체에 넣어주기 때문이다. 복사 대상인 객체가 불변인 경우 문제가 되지 않지만 가변인 경우 동일한 객체를 참조하게 되어 한쪽의 변경이 다른 쪽에도 영향을 미쳐 에측할 수 없는 오류를 일으키게 된다.&lt;/p&gt;
&lt;p&gt;따라서 아래와 같이 clone() 메서드를 수정해 주어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Override
public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(); // 도달 불가능
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
  &lt;p&gt;단, 이 경우 &lt;code&gt;elements&lt;/code&gt;가 final이 아니어야 한다는 문제가 있으며 이는 &lt;code&gt;가변 객체를 참조하는 필드는 final로 선언하라&lt;/code&gt;는 일반 용법과 충돌한다. 복제 가능한 클래스 구현을 위해서는 부득이하게 필드에서 final을 제거해야 될 수도 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그런데 만약 HashTable과 같은 클래스의 경우 필드인 배열이 가지고 있는 값이 연결 리스트의 첫번째 엔트리이다. 따라서 이 경우 단순히 배열만 복제한다고 해서 원본 객체와 복제본 객체가 완벽히 분리되지 못한다. 연결리스트를 구성하는 Entry 객체가 동일한 인스턴스가 되어버리므로 복제된 객체의 연결 리스트가 수정되면 당연히 원본 객체의 연결 리스트도 영향을 받는다.&lt;/p&gt;
&lt;p&gt;따라서 이런 경우엔, 연결 리스트를 구성하는 모든 엔트리를 새롭게 생성해주는 방식으로 복사를 진행해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

    private static class Entry {
        Object key, value;
        Entry next;

        Entry deepCopy() {
            return new Entry(key, value, next == null ? null : next.deepCopy());
        }
    }

    @Override public HashTable clone() {
        // try-catch 생략
        HashTable result = (HashTable) super.clone();
        result.buckets = new Entry[buckets.length]; // 새로운 배열 생성
        // 원소(Entry)들과 그 원소들의 next들을 순회하면서
        // 원본과 분리된 연결 리스트를 생성
        for (int i = 0; i &amp;lt; buckets.length ; i++) {
            if (buckets[i] != null) {
                result.buckets[i] = buckets[i].deepCopy();
            }
        }
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 원본이나 복제본의 buckets가 수정되어도 서로에게 영향을 주지 않게 된다.&lt;/p&gt;
&lt;h3 id=&quot;-배열-deep-copy&quot;&gt;+ 배열 deep copy&lt;/h3&gt;
&lt;p&gt;배열.clone()으로 배열을 복사하면 새로운 배열을 만들고 기존 배열의 원소를 채워넣어 반환해주기 때문에 배열의 원소를 삭제하거나 추가해도 서로의 배열엔 영향을 주지 않지만 배열이 갖는 값이 참조 객체인 경우 해당 객체의 값을 수정하면 원본 배열의 객체가 같이 바뀌게 된다. (동일한 인스턴스를 원소로 갖기 때문에)&lt;/p&gt;
&lt;p&gt;따라서, 정말 '깊은' 복사를 원한다면 배열.clone()으로는 불충분하고 새롭게 배열을 만들고 내부 원소들을 순회하면서 원소.clone()을 호출해주어야 원소들 각각이 변경되어도 서로에게 영향을 주지 않는다. 즉, 이렇게 하면 기존 배열과 새로운 배열은 내부 원소를 포함해서 전혀 다른 인스턴스가 되는 것이다.&lt;/p&gt;
&lt;h3 id=&quot;-쓰레드-안전-클래스&quot;&gt;+ 쓰레드 안전 클래스&lt;/h3&gt;
&lt;p&gt;Object.clone() 은 멀티 쓰레드 환경을 고려하지 않았으므로 쓰레드 안전한 클래스를 만들기 위해서는 clone() 메서드가 아무런 작업도 하지 않더라도 재정의하고 동기화를 해주어야 한다!&lt;/p&gt;
&lt;h3 id=&quot;-clone-에서는-재정의-가능한-메서드-호출-금지&quot;&gt;+ clone() 에서는 재정의 가능한 메서드 호출 금지&lt;/h3&gt;
&lt;p&gt;만약 clone() 에서 재정의 가능한 메서드를 호출하게 되면 하위 클래스에서 &lt;code&gt;super.clone()&lt;/code&gt;을 호출했을 때 상위 클래스의 &lt;code&gt;clone()&lt;/code&gt;에서 하위 클래스의 &lt;strong&gt;재정의 된&lt;/strong&gt; 메서드를 호출하게 되고 예측할 수 없는 복제본이 만들어질 가능성이 생긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class Parent implements Cloneable {

    protected int value = 0;

    @Override
    public Parent clone() {
        super.clone();
        // 재정의 가능한 메서드 호출
        overrideableMethod();
        ...
    }

    public void overrideableMethod() {
        value += 1;
    }
}

public class Child extends Parent {
    @Override
    public Parent clone() {
        super.clone();
        ...
    }

    @Override
    public void overrideableMethod() {
        value += 2;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같은 상황일 때&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;Parent instance = new Child();
instance.clone();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 실행하면 실제 인스턴스가 &lt;code&gt;Child&lt;/code&gt; 이기 때문에 Parent.clone()의 overrideableMethod()는 Child에 선언된 메서드를 호출한다. 즉, &lt;code&gt;Child.clone() -&amp;gt; Parent.clone() -&amp;gt; Child.overrideableMethod()&lt;/code&gt; 순으로 호출된다. 따라서 Parent 레벨에서 조정되어야 할 값들이 Child에 재정의된 메서드의 동작 방식대로 조정되고 의도치 않은 방향으로 값이 복제되는 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;따라서, clone() 메서드에서는 재정의 가능한 메서드는 호출해서는 안 된다. 메서드 호출이 필요하면 private 도우미 메서드로 만들어서 사용해야 한다.&lt;/p&gt;
&lt;h2 id=&quot;더-나은-방법---복사-생성자--복사-팩터리&quot;&gt;더 나은 방법 - 복사 생성자 &amp;amp; 복사 팩터리&lt;/h2&gt;
&lt;p&gt;확장하려는 클래스가 Cloneable을 구현한 경우 어쩔 수 없이 clone()을 재정의해줘야 하지만 그렇지 않은 상황에서는 복사 생성자 &amp;amp; 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class Car {

    // 복사 생성자
    public Car(Car car) {
        ...
        return newCar;
    }

    // 복사 팩터리
    public static Car newInstance(Car car) {
        ...
        return newCar;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 인자를 받기 때문에 구현 클래스가 아니라 인터페이스를 받을 수도 있고, 따라서 해당 인터페이스를 구현하는 클래스끼리는 다른 구현 클래스로의 복사도 가능해진다(HashSet -&amp;gt; TreeSet 등). 개인적으로는 정확하게 복사한다는 것을 명시해줄 수 있는 팩터리 방식이 더 좋아보인다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;이펙티브 자바 &lt;a href=&quot;https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94&quot;&gt;전체 아이템 목록&lt;/a&gt; (스터디 정리 레포지토리)&lt;/p&gt;
&lt;/blockquote&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/110</guid>
      <comments>https://gmelon.dev/110#entry110comment</comments>
      <pubDate>Mon, 19 Jun 2023 20:30:47 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - 그리디] 주식</title>
      <link>https://gmelon.dev/109</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;p&gt;문제 링크 - &lt;a href=&quot;https://www.acmicpc.net/problem/11501&quot;&gt;https://www.acmicpc.net/problem/11501&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;접근법&quot;&gt;접근법&lt;/h2&gt;
&lt;p&gt;정답을 얻기 위해서는 아래와 같이 계산하면 된다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;현재 주식 가격 이후에 더 비싼 가격이 있으면 -&amp;gt; 오늘 주식을 산다&lt;/li&gt;
&lt;li&gt;현재 주식이 남은 주식 가격 중 &lt;strong&gt;가장 비싼 가격이면&lt;/strong&gt; -&amp;gt; 현재까지 구매한 주식을 모두 판다&lt;/li&gt;
&lt;li&gt;앞으로 더 비싼 주식 가격이 없으면 -&amp;gt; 아무것도 하지 않는다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;문제는 이를 구현하는 방법인데, 처음에는 단순히 (주식 가격) 배열의 앞에서부터 순회하며 계산을 시도했다. 코드는 아래처럼 될 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// main()에서 입력 처리 후 solution을 부르도록 작성
public static int solution(final int days, final int[] prices) {
    PriorityQueue&amp;lt;Integer&amp;gt; queue = new PriorityQueue&amp;lt;&amp;gt;(Comparator.reverseOrder());
    for (int price : prices) {
        queue.add(price);
    }

    long answer = 0;
    List&amp;lt;Integer&amp;gt; currentStocks = new ArrayList&amp;lt;&amp;gt;();
    for (int price : prices) {
        if (!queue.isEmpty() &amp;amp;&amp;amp; price &amp;lt; queue.peek()) { // 이후에 더 비싼 주식 존재
            // 주식 구매 로직
            currentStocks.add(price); 
        } else if (!queue.isEmpty() &amp;amp;&amp;amp; price == queue.peek()) { // 남은 주식 중 최고가인 경우
            for (Integer integer : currentStocks) {
                // 주식 판매 로직
                answer += (price - integer);
            }
            // 구매했던 주식들 판매 완료했으므로 clear
            currentStocks.clear();
        }
        // 주식 가격에 관계없이 순회가 끝난 값은 우선순위큐에서 뺴주어야 함
        queue.remove(price);
    }
    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;남은 주식 중 가장 비싼 가격을 찾기 위해 우선순위 큐에 주식 가격들을 역순으로 넣고, &lt;code&gt;queue.peek()&lt;/code&gt; 으로 남은 주식 가격 중 최고가를 조회하면서 현재 주식 가격이 더 작으면 구매한다(list에 add). 그러다가 남은 주식 중 최고가를 만나면 지금까지 구매했던 주식들을 모두 판매한다.&lt;/p&gt;
&lt;p&gt;이렇게 계산하면 주어진 테스트 케이스에 올바른 답이 나오긴하나, 시간초과로 문제가 풀리지 않는다. 우선순위큐를 생성하는 비용과 매번 제거하는 비용, 주식을 판매할 때 순회하는 비용이 그 원인이었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/stbIj/btsjHIlK0En/WlHRUsA5UKXQuOvqXnmuOK/img.png&quot; alt=&quot;image-20230612174556418&quot; /&gt;&lt;/p&gt;
&lt;p&gt;배열의 앞에서부터 순회하면 항상 위와 같이 비효율적인 (구매한 주식을 팔기 위한 순회 등) 로직이 등장할 수밖에 없어보였다. 이 문제를 해결하기 위해서는 배열의 뒤에서부터 순회하면 된다. 뒤에서부터 순회하게 되면 &lt;strong&gt;주식을 팔아야 할 시점&lt;/strong&gt;과 &lt;strong&gt;주식을 판매할 때의 주식 가격&lt;/strong&gt;을 미리 알 수 있으므로 배열을 한 번 순회하기만 하면 최종 이익을 계산할 수 있으므로 O(N)에 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p&gt;그림으로 표현하면 아래와 같다.&lt;/p&gt;
&lt;h3 id=&quot;앞에서-부터-순회&quot;&gt;앞에서 부터 순회&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDnLMV/btsjx4qHl1r/ieijRULaCXgwyFZSaEKkwK/img.png&quot; alt=&quot;image-20230612180427793&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zuKAS/btsjFXjOe1p/2OLsuqlpvM8UTAAnKqTyEK/img.png&quot; alt=&quot;image-20230612180448926&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLSoMD/btsjAv2rfPr/sK7DI9UQQWko3zXSYRboSk/img.png&quot; alt=&quot;image-20230612180605542&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LVrTM/btsjAxeQ4km/AX2VKmI8WcbsyS6kOVllDk/img.png&quot; alt=&quot;image-20230612180635998&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(이후 생략)&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;때문에 주식을 판매할 때 순회가 발생하고 매번 남은 주식 중 최고가를 계산해야 한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;뒤에서-부터-순회&quot;&gt;뒤에서 부터 순회&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oz8nG/btsjILvA4Wg/TxCihskBYHRibJZmkePLtK/img.png&quot; alt=&quot;image-20230612180923127&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MfdEg/btsjFWLZF5A/kedTIyrErRBv9Dg9jK34XK/img.png&quot; alt=&quot;image-20230612181000211&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p4eRJ/btsjFXxm6t7/pdwXDO9sOge4rP28AJMR6k/img.png&quot; alt=&quot;image-20230612181023300&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSQQH7/btsjAv9bOqr/hFMERKDcTo1rzBlnOygWY0/img.png&quot; alt=&quot;image-20230612181039571&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caT6zm/btsjFS30ijA/H2kkAVKbwcjllrDD1W0IBK/img.png&quot; alt=&quot;image-20230612181113103&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GyaOm/btsjD9Sj3PL/t3klGnJS4uPgrV04yK1k40/img.png&quot; alt=&quot;image-20230612181137181&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(이후 생략)&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;때문에 뒤에서부터 한번만 순회하면 전체에서 최고 판매 이득을 구할 수 있게 된다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위 로직을 코드로 작성하면 아래와 같이 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static long solution(final int days, final int[] prices) {
    long count = 0;
    int prevMax = Integer.MIN_VALUE;

    for (int i = prices.length - 1; i &amp;gt;= 0; i--) {
        int current = prices[i];
        if (current &amp;gt; prevMax) {
            prevMax = current;
            continue;
        }

        if (current &amp;lt; prevMax) {
            count += (prevMax - current);
        }
    }

    return count;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한가지 주의할점은 입력 값의 범위가 &lt;code&gt;2 &amp;lt;= N &amp;lt;= 1,000,000&lt;/code&gt; 이고 &lt;code&gt;0 &amp;lt;= 주가 &amp;lt;= 10,000&lt;/code&gt; 이므로 이익이 int 범위를 벗어날 수 있어서 &lt;code&gt;count&lt;/code&gt;를 &lt;code&gt;int&lt;/code&gt;로 선언할 경우 오답 처리된다. (문제에서도 답이 &lt;code&gt;부호있는 64bit 정수형&lt;/code&gt;으로 표현 가능하다고 언급되어 있음) 따라서 &lt;code&gt;count&lt;/code&gt;의 타입을 &lt;code&gt;long&lt;/code&gt;으로 선언해주어야 문제 없이 정답 처리된다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/109</guid>
      <comments>https://gmelon.dev/109#entry109comment</comments>
      <pubDate>Mon, 12 Jun 2023 18:16:09 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - 그리디] 무지의 먹방 라이브</title>
      <link>https://gmelon.dev/107</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;p&gt;문제 링크 - &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42891&quot;&gt;프로그래머스&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ6IWE/btsjjA3GJVc/sWhL0LBgBanFFbEmorTXNk/img.png&quot; alt=&quot;image-20230609191223357&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그렇다고 합니다. 무지가 갑자기 얄미워보이는 마법의 코테&lt;/p&gt;
&lt;p&gt;결국 정리하면 0, 1, …, n초 동안 주어진 음식을 먹는데 k초뒤에 어떤 음식을 먹고있을지 출력하는 문제이다. 단순하게 k번 루프를 돌리면 답이 나오긴 하곘지만 입력 범위가 엄청 크기 때문에 다른 방법을 써야 될 것 같았다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYylZ5/btsjkFKw6v7/4klRdB1sugiTgxLZADVJxk/img.png&quot; alt=&quot;image-20230609213845765&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;풀이-방법&quot;&gt;풀이 방법&lt;/h2&gt;
&lt;p&gt;그래서 처음에 생각한 방법은 이렇다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;LinkedList에 주어진 음식(섭취에 걸리는 시간)들을 모두 넣어 오름차순으로 정렬&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;정렬되기 이전 순서대로 ArrayList로도 하나 만들어두고 두 list가 같은 객체들을 참조하도록 해준다&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;루프안에서 현재 count를 기억해두고 현재와 동일한 시간을 갖는 음식을 &lt;code&gt;removeFirst() - O(1)&lt;/code&gt; 으로 제거&lt;ol&gt;
&lt;li&gt;이때 음식의 시간을 0으로 변경해주고 제거하기 (나중에 ArrayList에서 순회 시 사용)&lt;/li&gt;
&lt;li&gt;모든 원소를 순회하는게 아니라 현재 count와 동일한 원소만 순회하므로 시간이 줄어든다&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;네트워크가 중단될 시간 k에서 현재 LinkedList 크기를 뺴준다 (원소 개수만큼의 시간동안 음식을 먹은 셈이므로)&lt;/li&gt;
&lt;li&gt;만약, 매 순회 시 남은 k가 LinkedList의 크기보다 작다면, &lt;strong&gt;남은 음식을 모두 먹기 전에 k초에 도달한다는 의미&lt;/strong&gt;이므로 남은 음식 중 k번째 음식을 반환한다&lt;ol&gt;
&lt;li&gt;이때, &lt;strong&gt;정답을 반환할 때 이미 다 먹은 음식의 index도 고려해야 하므로&lt;/strong&gt; 앞서 만들어둔 정렬되지 않은 ArrayList를 순회하며 음식시간이 0이 되지 않은 음식을 만날 때 마다 &lt;code&gt;k--&lt;/code&gt; 를 해주고 &lt;code&gt;k == 0&lt;/code&gt; 이 되는 순간의 음식 index를 반환하면 답을 찾을 수 있다.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;만약, &lt;code&gt;k == 0&lt;/code&gt;이 됨과 동시에 남은 음식이 없는 경우 혹은 &lt;code&gt;k &amp;gt; 0&lt;/code&gt; 인데 음식이 남아있는 경우 더 이상 먹방을 진행할 수 없으므로 -1을 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위 방법으로 작성한 코드는 아래와 같고, 정확성 / 효율성 테스트 모두 일단 통과했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;import java.util.*;
import java.util.stream.*;

class Solution {

    static class Food implements Comparable&amp;lt;Food&amp;gt; {
        public int time = 0;

        public Food(int time) {
            this.time = time;
        }

        @Override
        public int compareTo(Food other) {
            return this.time - other.time; // 오름차순
        }
    }

    public int solution(int[] food_times, long k) {
        List&amp;lt;Food&amp;gt; foods = Arrays.stream(food_times)
            .mapToObj(Food::new)
            .collect(Collectors.toList());

        LinkedList&amp;lt;Food&amp;gt; sortedFoods = new LinkedList&amp;lt;&amp;gt;(foods);
        Collections.sort(sortedFoods);

        int iterCount = 0;
        while (k &amp;gt;= 0 &amp;amp;&amp;amp; sortedFoods.size() &amp;gt; 0) {
            iterCount++;
            if (k &amp;gt;= sortedFoods.size()) {

                k -= sortedFoods.size();

                while (!sortedFoods.isEmpty()) {
                    Food current = sortedFoods.getFirst();
                    if (current.time &amp;lt;= iterCount) {
                        current.time = 0;
                        sortedFoods.removeFirst();
                    } else {
                        break;
                    }
                }   
            } else {
                int index = -1;
                while (k &amp;gt;= 0) {
                    index++;
                    if (foods.get(index).time == 0) {
                        continue;
                    }
                    k--;
                }
                return index + 1; // 문제의 index는 1부터 시작
            }
        }
        // 더 이상 섭취할 수 있는 음식이 없는 경우
        return -1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;개선-해보기&quot;&gt;개선(?) 해보기&lt;/h2&gt;
&lt;p&gt;현재는 Food가 index를 포함하지 않아서 index를 별도로 계산해주어야 하고 리스트도 2개하다는 문제가 있다. 이를 개선해볼 수 있을 것 같았다.&lt;/p&gt;
&lt;p&gt;먼저 Food를 이렇게 변경해줬다&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;static class Food {
    public int time;
    public int index;

    public Food(int time, int index) {
        this.time = time;
        this.index = index;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 LinkedList를 먼저 시간으로 정렬한 후 기존에 k와 리스트를 업데이트하는 작업을 수행하고 k가 남은 음식 수보다 작을 때 수행하는 연산은 다시 해당 List를 index로 정렬 후 수행해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;import java.util.*;
import java.util.stream.*;

class Solution {

    static class Food {
        public int time;
        public int index;

        public Food(int time, int index) {
            this.time = time;
            this.index = index;
        }
    }

    public int solution(int[] food_times, long k) {
        LinkedList&amp;lt;Food&amp;gt; foods = new LinkedList&amp;lt;&amp;gt;();
        for(int i = 0 ; i &amp;lt; food_times.length ; i++) {
            foods.add(new Food(food_times[i], i + 1));
        }

        // 먼저 시간으로 오름차순 정렬
        Collections.sort(foods, (f1, f2) -&amp;gt; f1.time - f2.time);

        int iterCount = 0;
        while (k &amp;gt;= 0 &amp;amp;&amp;amp; foods.size() &amp;gt; 0) {
            iterCount++;
            if (k &amp;gt;= foods.size()) {

                k -= foods.size();

                while (!foods.isEmpty()) {
                    Food current = foods.getFirst();
                    if (current.time &amp;lt;= iterCount) {
                        current.time = 0;
                        foods.removeFirst();
                    } else {
                        break;
                    }
                }   
            } else {
                // index로 오름차순 정렬
                Collections.sort(foods, (f1, f2) -&amp;gt; f1.index - f2.index);
                return foods.get((int) k).index;
            }
        }
        // 더 이상 섭취할 수 있는 음식이 없는 경우
        return -1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드는 간결해졌는데 sort를 두번 수행하게 되어 어째 시간복잡도는 늘어난거같다. 획기적으로 시간을 줄일 수 있는 코드도 있는 것 같은데 아직 그런 풀이를 스스로 떠올리지는 못하겠어서 그리디 문제를 좀 더 풀어보고 다시 고민해보면 좋을 것 같다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/107</guid>
      <comments>https://gmelon.dev/107#entry107comment</comments>
      <pubDate>Fri, 9 Jun 2023 21:38:06 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] Item 10 - equals는 일반 규약을 지켜 재정의하라</title>
      <link>https://gmelon.dev/106</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;p&gt;아이템 10번은 equals를 재정의할 때 지켜야 할 규칙들과 주의해야 하는 점들에 대해 설명하고 있다. 자바에서 equals()를 재정의하지 않으면 오직 자기 자신의 인스턴스와만 같게 되기 때문에 필요한 경우 equals() 메서드를 재정의해서 사용해야 한다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;책에서는 equals가 만족해야 하는 규칙들 모두에 대해 상황을 제시해 예제 코드로 설명하고 있지만 이를 모두 정리할 필요는 없을 것 같아 중요한 내용만 옮겼으니 자세한 내용은 책을 참고하시길 바랍니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;equals가-필요-없는-상황&quot;&gt;equals가 필요 없는 상황&lt;/h2&gt;
&lt;p&gt;equals() 를 재정의해서 불필요한 문제가 발생할 수도 있으므로 아래의 경우에 해당된다면 equals()를 아예 재정의하지 않는 것도 좋은 방법이다. 이 경우 기본 &lt;code&gt;Object.equals()&lt;/code&gt; 가 호출되어 (&lt;strong&gt;주소 값 비교&lt;/strong&gt;) 오직 자기 자신의 인스턴스와만 같다는 결과가 나오게 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 인스턴스가 &lt;strong&gt;본질적으로&lt;/strong&gt; 고유 (쓰레드 등)&lt;/li&gt;
&lt;li&gt;설계 상 &lt;strong&gt;동등성&lt;/strong&gt;을 검사할 필요가 없는 경우&lt;/li&gt;
&lt;li&gt;상위 클래스 (주로 abstract) 의 equals()가 하위 클래스에도 딱 들어맞는 경우&lt;ul&gt;
&lt;li&gt;자바 API의 경우 &lt;code&gt;AbstractList -&amp;gt; List&lt;/code&gt;, &lt;code&gt;AbstractMap -&amp;gt; Map구현체들&lt;/code&gt; 이 이렇게 구현되어 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;equals가-필요한-상황&quot;&gt;equals가 필요한 상황&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;객체 식별성&lt;/code&gt; (두 객체가 &lt;code&gt;물리적&lt;/code&gt;으로  같은가, &lt;code&gt;동일성&lt;/code&gt;) 이 아니라 &lt;code&gt;논리적 동치성 (동등성)&lt;/code&gt;을 확인해야 하는데 상위 클래스의 equals() 가 이를 확인하도록 재정의되어 있지 않을 때는 필수로 equals()를 재정의해야 한다. 주로 Integer, String과 같은 &lt;code&gt;값 클래스&lt;/code&gt; 들이 여기에 해당한다.&lt;/p&gt;
&lt;p&gt;값 클래스의 경우 주소가 a이고 값이 1인 객체와 주소가 b이고 값이 1인 객체는 동일한 객체로 판단되어야 하므로 equals() 구현이 필요한 상황이라고 할 수 있다.&lt;/p&gt;
&lt;p&gt;값 클래스가 아니고 불변이 아니더라도 주소가 아닌 현재 객체의 상태에 따라 동등성을 확인하고자 하는 클래스라면 equals()를 재정의하고 핵심 필드의 값을 비교하도록 구현해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;equals가-만족해야-하는-규칙들&quot;&gt;equals가 만족해야 하는 규칙들&lt;/h3&gt;
&lt;p&gt;equals()는 아래의 일반 규약을 지켜 재정의되어야 한다. &lt;code&gt;자바 컬렉션을 포함한 다른 수많은 클래스들은 equals()가 아래 규약을 지켰다고 가정하고 동작&lt;/code&gt;하므로 이를 지키는 것은 매우 중요하며, 지키지 않을 경우 개발자의 의도와 다르게 프로그램이 동작할 수 있다. &lt;/p&gt;
&lt;p&gt;예를 들어 equals()를 구현하지 않은 값 클래스를 List에 넣고 동일한 값을 같는 새로운 인스턴스를 만들고 인자로 넘겨서 List.contains()를 호출하면 false가 나오게 된다. 반면, equals()를 제대로 구현하면 true가 나온다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;MyClass myClassA = new MyClass(3);

List&amp;lt;MyClass&amp;gt; myClasses = new ArrayList&amp;lt;&amp;gt;();
myClasses.add(myClassA);

/**
 * equals() 구현 X -&amp;gt; false
 * equals() 구현 O -&amp;gt; true
 */
myClasses.contains(new MyClass(3));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래에서 x, y, z는 각각 &lt;code&gt;null이 아닌 참조 값&lt;/code&gt;이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;반사성&lt;ul&gt;
&lt;li&gt;모든 x에 대해, x.equals(x) 는 true&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;대칭성&lt;ul&gt;
&lt;li&gt;모든 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;추이성&lt;ul&gt;
&lt;li&gt;모든 x, y, z에 대해 x.equals(y)가 true고 y.equals(z)도 true면 x.equals(z)도 true&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;일관성&lt;ul&gt;
&lt;li&gt;x.equals(y)를 반복 호출해도 계속 같은 결과가 나와야 힘&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;null-아님&lt;ul&gt;
&lt;li&gt;모든 x에 대해 x.equals(null)은 false&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;상속-관계에서의-equals&quot;&gt;상속 관계에서의 equals&lt;/h3&gt;
&lt;p&gt;부모 클래스가 있고, 이를 상속받는 하위 클래스에서 새로운 필드를 가질 때 이 두 클래스에 대한 equals() 재정의는 굉장히 까다롭다. 책에서 아주 자세히 설명하고 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 Point와 이를 상속받는 ColorPoint가 있다고 할 때 하위 클래스는 항상 부모 클래스로 대체되어 사용가능해야 하므로 (리스코프 치환 원칙, LSP) 아래와 같이 사용이 가능하고 각 클래스의 equals()도 이를 고려하여 설계되어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;class Point {
    private int x, y;
    // 생성자 등
}

class ColorPoint extends Point {
    private String color;
    // 생성자 등
}

Point point = new Point(1, 1);
Point colorPoint = new ColorPoint(1, 1, &quot;red&quot;);

point.equals(colorPoint); // true가 되어야 함
colorPoint.equals(point); // true가 되어야 함&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 위해 아래와 같이 Point와 ColorPoint의 equals()를 구현했다고 해보자.&lt;/p&gt;
&lt;h4 id=&quot;point&quot;&gt;Point&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) {
        return false;
    }
    Point p = (Point) o; // 항상 성공
    return p.x == x &amp;amp;&amp;amp; p.y == y;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;colorpoint&quot;&gt;ColorPoint&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) {
        return false;
    }

    if (!(o instanceof ColorPoint)) {
        // 주어진 o가 Point면 Point의 equals()로 비교
        return o.equals(this);
    }

    // 주어진 o가 ColorPoint면 색상까지 비교
    ColorPoint c = (ColorPoint) o;
    return super.equals(o) &amp;amp;&amp;amp; c.color.equals(color);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;언뜻 보기에는 문제 없이 작동할 것 같지만, 아래와 같은 경우에 추이성이 깨지는 문제가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;ColorPoint obj1 = new ColorPoint(1, 2, &quot;red&quot;);
Point obj2 = new Point(1, 2);
ColorPoint obj3 = new ColorPoint(1, 2, &quot;green&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;equals()의 일반 규약 중 추이성에 따르면 obj1.equals(obj2) 가 true 이고 obj2.equals(obj3) 가 true 이므로 obj1.equals(obj3) 도 true 여야 한다. 하지만 마지막 equals() 문은 false를 반환한다. ColorPoint 끼리의 비교는 &lt;code&gt;color&lt;/code&gt; 필드까지도 비교하기 때문이다.&lt;/p&gt;
&lt;p&gt;책에서는 이 밖에도 다양한 경우의 (문제 있는) 예제 코드를 제시하며 상속 관계에서 하위 클래스가 필드를 추가한다면 정상적으로 equals 규약을 만족시킬 방법이 없다고 이야기한다. 그리고 그에 대한 해결책으로 &lt;code&gt;상속 대신 컴포지션을 사용&lt;/code&gt;하라고 말한다.&lt;/p&gt;
&lt;p&gt;컴포지션을 사용해 ColorPoint를 아래와 같이 구현한다면, ColorPoint의 equals() 에서 현재 인스턴스가 Point 인지 여부를 확인해줄 필요가 없기 때문에 앞서 이야기한 문제들이 전혀 발생하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 컴포지션을 사용한 ColorPoint
public final class ColorPoint {
    private Point point;
    private String color;

    public ColorPoint(int x, int y, String color) {
        this.point = new Point(x, y);
        this.color = color;
    }

    // Point 뷰를 반환하는 메서드를 제공하여
    // ColorPoint가 Point로서 활용될 수 있도록 함
    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object o) {
        // ColorPoint가 아니면 무조건 false를 반환할 수 있게 된다.
        if (!(o instanceof ColorPoint)) {
            return false;
        }
        ColorPoint c = (ColorPoint) o;
        return c.point.equals(this.point) &amp;amp;&amp;amp; c.color.equals(this.color);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ColorPoint의 equals() 메서드에 무조건 ColorPoint만 인자로 전달되므로 위와 같이 equals() 메서드를 작성할 수 있게 되었다.  &lt;code&gt;asPoint()&lt;/code&gt; 와 같은 메서드를 제공해 ColorPoint가 Point로 활용되어야할 경우도 대응할 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;비슷한 이유로 &lt;code&gt;상위 클래스가 추상 클래스&lt;/code&gt;인 경우에도 해당 클래스가 인스턴스화 될 수 없으므로 equals() 구현 시 문제가 발생하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;equals-구현-시-주의점&quot;&gt;equals 구현 시 주의점&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;equals() 비교에 신뢰할 수 없는 자원 사용하지 않기&lt;ol&gt;
&lt;li&gt;ex) url 대신 이에 매핑되는 ip주소로 equals() 수행&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;null도 정상 값으로 취급하는 객체는 &lt;code&gt;Objects.equals()&lt;/code&gt;로 비교하기&lt;ol&gt;
&lt;li&gt;Objects.equals()는 아래와 같이 구현되어 있어 호출 대상 객체가 null인 경우도 대응할 수 있다.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null &amp;amp;&amp;amp; a.equals(b));
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;다를 가능성이 크거나 비교 비용이 저렴한 필드부터 비교하기&lt;ol&gt;
&lt;li&gt;해당 필드의 값이 다를 경우 이후 필드를 계산하지 않고 바로 false를 반환할 수 있다&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;hashcode도 반드시 재정의하기 (Item 11)&lt;ol&gt;
&lt;li&gt;hashcode가 재정의되어 있지 않을 경우 hash를 사용하는 컬렉션의 성능이 저하될 수 있다.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;equals()의 인자는 반드시 Object여야 한다&lt;ol&gt;
&lt;li&gt;예를 들어 아래와 같이 하게 되면 재정의가 아닌 다중정의가 되므로 반드시 Object로 해야 한다&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public boolean equals(MyClass o) {
    // 금지
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;IDE가 만들어주는 equals()를 애용하자!&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;blockquote&gt;
  &lt;p&gt;이펙티브 자바 &lt;a href=&quot;https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94&quot;&gt;전체 아이템 목록&lt;/a&gt; (스터디 정리 레포지토리)&lt;/p&gt;
&lt;/blockquote&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/106</guid>
      <comments>https://gmelon.dev/106#entry106comment</comments>
      <pubDate>Thu, 8 Jun 2023 21:46:11 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] Item 7 - 다 쓴 객체 참조를 해제하라</title>
      <link>https://gmelon.dev/105</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;개요&quot;&gt;개요&lt;/h2&gt;
&lt;p&gt;자바는 가비지 컬렉터가 있으므로 메모리 관리에 전혀 신경쓰지 않아도 된다고 생각하지만 이는 사실이 아니다! &lt;/p&gt;
&lt;p&gt;예를 들어 스택을 아래와 같이 구현하면 지속적인 메모리 누수가 발생해 프로그램이 종료될 수도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class Stack {
    private Object[] elements;
    private int size = 0;

    // 생성자

    public void push(Object obj) {
        ensureCapacity(); // 배열이 모자라면 늘리기
        elements[size++] = obj;
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return elements[--size];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같은 스택은 &lt;code&gt;elements&lt;/code&gt; 배열이 사라지지 않고 메모리에 계속 누적되며 객체의 참조를 가지고 있게 되므로 가비지 컬렉터가 작동하지 못한다.&lt;/p&gt;
&lt;h3 id=&quot;메모리-관리-방법&quot;&gt;메모리 관리 방법&lt;/h3&gt;
&lt;p&gt;가장 간단한 방법은 해당 참조 변수를 null로 선언해버리는 것이다. 그럼 더 이상 Heap 영역에 저장된 객체에 대한 참조가 존재하지 않기 때문에 가비지 컬렉터가 작동할 수 있다. 하지만 명시적으로 변수를 null로 선언하는 것은 가장 마지막에 고려되어야 하고(위 Stack과 같이 &lt;code&gt;원소 풀을 직접 관리&lt;/code&gt;할 때 사용할 수 있다), 일반적으로는 변수를 &lt;code&gt;scope 밖으로 밀어버리는 방법&lt;/code&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;캐시&quot;&gt;캐시&lt;/h2&gt;
&lt;p&gt;캐시 역시 메모리 누수를 일으키는 주범이다. 캐시에 객체의 참조를 넣고 그대로 잊어버리게 되면 마찬가지로 가비지 컬렉터가 동작할 수 없기 때문에 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;이때는 여러 가지 해법이 있다. 먼저, 키가 참조되는 동안만 엔트리가 살아있는 캐시가 필요한 것이라면 &lt;code&gt;WeakHashMap&lt;/code&gt;을 사용할 수 있다. 이를 이해하기 위해선 먼저 &lt;code&gt;참조 유형&lt;/code&gt;에 대해 알아야 한다.&lt;/p&gt;
&lt;h3 id=&quot;참조-유형&quot;&gt;참조 유형&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;자바의 참조 유형에는 4가지 종류가 있음&lt;ul&gt;
&lt;li&gt;참조 유형에 따라 GC 실행 여부와 시점이 달라짐&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;strong-reference&quot;&gt;Strong Reference&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;자바의 기본 참조 유형&lt;/li&gt;
&lt;li&gt;어떤 변수가 &lt;strong&gt;객체에 대한 참조&lt;/strong&gt;를 가지고 있는 한, 해당 객체는 GC의 대상이 되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;MyClass obj = new MyClass(); // obj가 new MyClass() 를 들고 있으므로 GC X

obj = null; // 이 시점부터 new MyClass() 인스턴스가 gc의 대상이 됨&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;soft-reference&quot;&gt;Soft Reference&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;SoftReference를 통해 들고 있는 경우 (다른 변수에 할당되어 있으면 GC 안 됨, 강한 참조)&lt;/li&gt;
&lt;li&gt;JVM 메모리가 부족한 &lt;strong&gt;경우에만&lt;/strong&gt; gc의 대상이 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;SoftReference&amp;lt;MyClass&amp;gt; obj = new SoftReference&amp;lt;&amp;gt;(new MyClass());

// 메모리가 부족하면 new MyClass() 인스턴스는 GC의 대상&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;weak-reference&quot;&gt;Weak Reference&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;WeakReference를 통해 들고 있는 경우 (다른 변수에 할당되어 있으면 GC 안 됨, 강한 참조)&lt;/li&gt;
&lt;li&gt;JVM 메모리에 &lt;strong&gt;관게 없이&lt;/strong&gt; gc의 대상이 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;WeakReference&amp;lt;MyClass&amp;gt; obj = new WeakReference&amp;lt;&amp;gt;(new MyClass());

// 메모리에 관계 없이 new MyClass() 인스턴스는 항상 GC의 대상&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;phantom-reference&quot;&gt;Phantom Reference&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;생략 (잘 사용되지 않는다고 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;weakhashmap의-동작-방식&quot;&gt;WeakHashMap의 동작 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Map에 (key, value)를 넣을 때 VO가 아닌 key에 대한 참조가 사라진다면 key를 통해서는 value에 접근할 수 있는 방법이 없다.&lt;ul&gt;
&lt;li&gt;하지만 일반적인 HashMap은 value를 계속 가지고 있는다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;WeakHashMap은 내부적으로 WeakReference를 사용해 Map에 삽입된 Entry 중 Key에 대한 참조가 null이 될 경우 gc할 때 value까지 삭제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;weakhashmap-코드-일부&quot;&gt;WeakHashMap 코드 일부&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;private static class Entry&amp;lt;K,V&amp;gt; extends WeakReference&amp;lt;Object&amp;gt; implements Map.Entry&amp;lt;K,V&amp;gt; {
    V value;
    final int hash;
    Entry&amp;lt;K,V&amp;gt; next;


    Entry(Object key, V value,
          ReferenceQueue&amp;lt;Object&amp;gt; queue,
          int hash, Entry&amp;lt;K,V&amp;gt; next) {
        super(key, queue); // HashMap의 key를 WeakReference의 생성자로 전달한다
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;동작-예시&quot;&gt;동작 예시&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;WeakHashMap&amp;lt;MyClass, String&amp;gt; map = new WeakHashMap&amp;lt;&amp;gt;();

MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();

map.put(obj1, &quot;obj1&quot;);
map.put(obj2, &quot;obj2&quot;);

obj1 = null; // obj1에 대한 참조가 WeakHashMap 내의 WeakReference만 남게 됨 -&amp;gt; gc의 대상이 됨

System.gc(); // 항상 gc를 보장하진 않음, gc가 이뤄졌다고 가정

map.keySet()
    .forEach(key -&amp;gt; System.out.println(map.get(key))); // obj2&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;다시 돌아와서 일반적인 캐시는 이와 달리, 유효 기간을 정확히 정의하기 어려우므로 시간이 지날 수록 엔트리의 가치를 떨어뜨리는 방식을 사용해야 한다. 이를 위해 &lt;code&gt;LinkedHashMap&lt;/code&gt;는 &lt;code&gt;removeEldestEntry()&lt;/code&gt; 메서드를 제공한다. 기본 구현체는 아래와 같이 false를 반환해 엔트리를 삭제하지 않지만,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;protected boolean removeEldestEntry(Map.Entry&amp;lt;K,V&amp;gt; eldest) {
    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자바에서 제공하는 일부 캐시 클래스들은 LinkedHashMap을 상속받고 이 메서드를 상황에 맞게 재정의해서 사용하도록 구현되어 있는 것 같다. 예를 들어 FileMemData의 내부 클래스 Cache는 아래와 같이 재정의하고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Override
protected boolean removeEldestEntry(Map.Entry&amp;lt;K, V&amp;gt; eldest) {
    if (size() &amp;lt; size) {
        return false;
    }
    CompressItem c = (CompressItem) eldest.getKey();
    c.file.compress(c.page);
    return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;리스너-콜백&quot;&gt;리스너, 콜백&lt;/h2&gt;
&lt;p&gt;마지막으로 리스너나 콜백을 등록한 후 해제하지 않아도 메모리 누수가 발생할 수 있다. 이러한 문제는 등록되는 참조 객체를 WeakReference로 선언해 해결할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;weakreference를-적용하지-않은-콜백-예시&quot;&gt;WeakReference를 적용하지 않은 콜백 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@FunctionalInterface
interface Callback {
    void callbackMethod();
}

class Callee {
    private Callback callback;

    public void setCallback(Callback callback) {
        this.callback = callback;
    }

    private callbackConditional() {
        // 콜백 메서드 호출이 필요한 상황에 호출
        callbackMethod();
    }
}

class Caller {
    private Callee callee;
    private Callback callback = () -&amp;gt; {
        System.out.println(&quot;callback method called&quot;);
    };

    public Caller() {
        callee.setCallback(callback);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드와 같은 상황이면 Callee가 Callback 객체를 계속해서 강한 참조로 들고 있기 때문에 Caller에서 여러 Callee에 Callback을 등록하면 직접 메서드를 통해 해제하지 않는 이상 Caller가 사라져도 (Callee 쪽에서 참조하기 때문에) Callback 객체가 GC 되지 않는다.&lt;/p&gt;
&lt;p&gt;아래와 같이 Callee 쪽에서 &lt;code&gt;WeakReference&amp;lt;Callback&amp;gt;&lt;/code&gt; 을 사용하면 Caller 가 사라질 때 (Callback에 대한 참조가 Callee의 WeakReference만 남으면) Callback 객체가 GC의 대상이 될 수 있도록 할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;weakreference를-적용한-콜백-예시&quot;&gt;WeakReference를 적용한 콜백 예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@FunctionalInterface
interface Callback {
    void callbackMethod();
}

class Callee {
    private WeakReference&amp;lt;Callback&amp;gt; callback;

    public void setCallback(Callback callback) {
        this.callback = new WeakReference&amp;lt;&amp;gt;(callback);
    }

    private callbackConditional() {
        // 콜백 메서드 호출이 필요한 상황에 호출
        callbackMethod();
    }
}

class Caller {
    private Callee callee;
    private Callback callback = () -&amp;gt; {
        System.out.println(&quot;callback method called&quot;);
    };

    public Caller() {
        callee.setCallback(callback);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
  &lt;p&gt;이펙티브 자바 &lt;a href=&quot;https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94&quot;&gt;전체 아이템 목록&lt;/a&gt; (스터디 정리 레포지토리)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;참고-자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lion-king.tistory.com/entry/Java-%EC%B0%B8%EC%A1%B0-%EC%9C%A0%ED%98%95-Strong-Reference-Soft-Reference-Weak-Reference-Phantom-References&quot;&gt;https://lion-king.tistory.com/entry/Java-%EC%B0%B8%EC%A1%B0-%EC%9C%A0%ED%98%95-Strong-Reference-Soft-Reference-Weak-Reference-Phantom-References&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bepoz-study-diary.tistory.com/340&quot;&gt;https://bepoz-study-diary.tistory.com/340&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.dreamy.pe.kr/zbxe/CodeClip/3768942&quot;&gt;http://www.dreamy.pe.kr/zbxe/CodeClip/3768942&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/105</guid>
      <comments>https://gmelon.dev/105#entry105comment</comments>
      <pubDate>Tue, 23 May 2023 12:18:13 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] Item 4 - 인스턴스화를 막으려거든 private 생성자를 사용하라</title>
      <link>https://gmelon.dev/104</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;개요&quot;&gt;개요&lt;/h2&gt;
&lt;p&gt;정적 메서드와 정적 필드만을 담은 클래스는 객체 지향적으로 보이지 않긴하지만 자바 API의 Arrays나 Collections가 그런 것처럼 특정 인터페이스를 구현하는 객체의 정적 팩토리 메서드를 넣어둘 수도 있고 (자바 8부터는 인터페이스에서도 가능) 상속이 불가능한 final 클래스와 관련된 메서드를 구현하고 모아둘 때도 사용한다.&lt;/p&gt;
&lt;p&gt;이러한 클래스는 인스턴스로 만들어 쓰려고 설계한게 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어준다.&lt;/p&gt;
&lt;h4 id=&quot;컴파일-전&quot;&gt;컴파일 전&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class UtilClass {
    private static int value = 0;

    public static int getValue() {
        return value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;컴파일-후&quot;&gt;컴파일 후&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class UtilClass {
    private static int value = 0;

    public UtilClass() {
    }

    public static int getValue() {
        return value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;해결-방안&quot;&gt;해결 방안&lt;/h2&gt;
&lt;p&gt;이러한 일을 방지하기 위해 private 생성자를 만들어주자. 그럼 컴파일러가 자동으로 기본 생성자를 만들지 않기 때문에 불필요한 인스턴스화를 막을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class UtilClass {
    private static int value = 0;

    private UtilClass() {
        throw new AssertionError();
    }

    public static int getValue() {
        return value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드처럼 예외 까지 던져주면 실수로 내부에서 인스턴스를 생성하는 것도 방지할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;상속-방지-효과&quot;&gt;상속 방지 효과&lt;/h2&gt;
&lt;p&gt;private 생성자만 하나 두는 방식은 상속을 금지시키는 효과도 있다. 모든 클래스의 생성자는 명시/묵시적으로 상위 클래스의 생성자를 super()를 통해 호출해야 하는데 private 생성자는 자식에서도 호출이 불가능하니 상속이 불가능해진다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;이펙티브 자바 &lt;a href=&quot;https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94&quot;&gt;전체 아이템 목록&lt;/a&gt; (스터디 정리 레포지토리)&lt;/p&gt;
&lt;/blockquote&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/104</guid>
      <comments>https://gmelon.dev/104#entry104comment</comments>
      <pubDate>Wed, 17 May 2023 21:23:33 +0900</pubDate>
    </item>
    <item>
      <title>DTO의 사용범위</title>
      <link>https://gmelon.dev/103</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;배경&quot;&gt;배경&lt;/h2&gt;
&lt;p&gt;넥스트스텝의 &lt;strong&gt;학습테스트로 배우는 Spring&lt;/strong&gt; 과정을 수강하고 있는데, 과정 진행 중 controller 패키지의 DTO를 어떤 계층까지 사용하면 좋을지에 대한 고민이 들었다. 처음엔 간단하게 생각했지만 생각보다 고민할 내용이 많았어서 고민한 내용과 결론을 정리해보려고 한다.&lt;/p&gt;
&lt;h2 id=&quot;dto란&quot;&gt;DTO란&lt;/h2&gt;
&lt;p&gt;먼저, DTO는 Data Transfer Object의 약자로 말그대로 &lt;strong&gt;계층 간에 데이터를 주고 받기 위해 사용하는 객체&lt;/strong&gt;이다. 보통 비지니스 로직은 두지 않고 필드와 그에 대한 getter와 생성자 등만 구현해두고 사용하는 경우가 많다.&lt;/p&gt;
&lt;p&gt;예를 들어 아래와 같은 객체가 DTO이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@NoArgsConstructor
@Getter
public class PlayRequestDto {

    private String names;
    private int count;

    @Builder
    public PlayRequestDto(String names, int count) {
        this.names = names;
        this.count = count;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;dto의-사용-범위&quot;&gt;DTO의 사용 범위&lt;/h2&gt;
&lt;h3 id=&quot;controller에서만-사용&quot;&gt;Controller에서만 사용&lt;/h3&gt;
&lt;p&gt;개발 전 설계를 하면서 dto를 어디까지 사용하도록 해야할지 고민했다. 그리고 먼저 아래와 같은 이유때문에 controller까지만 RequestDto를 사용하자고 결론을 내렸다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;controller dto를 서비스 계층까지 가지고 가게 되면 서비스 로직의 변화가 api 스펙의 변화를 야기할 수 있다&lt;/li&gt;
&lt;li&gt;해당 도메인의 서비스 도메인을 다른 도메인에서 사용하게 될 수도 있는데 특정 api 스펙에 특화된 controller dto를 service까지 가져가면 이러한 서비스 객체의 모듈화가 어렵다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그래서 구현된 controller 코드는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@RequiredArgsConstructor
@Controller
public class PlayController {
    private final PlayService playService;

    @PostMapping(&quot;/plays&quot;)
    public PlayResponseDto plays(@RequestBody PlayRequetDto requestDto) {
        List&amp;lt;PlayResult&amp;gt; playResults = playService.play(splitNames(playRequestDto.getNames()), playRequestDto.getCount());
        // 중략
        return new PlayResponseDto(winners, racingCars);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드를 보면 client에서 요청 body에 넣어 보낸 PlayRequestDto의 값을 getter로 꺼내 playService에 전달하고 있다.&lt;/p&gt;
&lt;p&gt;PlayService의 코드도 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Service
public class PlayService {
    public List&amp;lt;PlayResult&amp;gt; play(String carNames, int playCount) {
        RacingCarGame racingCarGame = new RacingCarGame(carNames, playCount);
        List&amp;lt;PlayResult&amp;gt; playResults = Collections.emptyList();

        while (!racingCarGame.isEnd()) {
            racingCarGame.play(movingStrategy);
            playResults = racingCarGame.getPlayResults();
        }

        return playResults;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dto를 Controller까지만 사용하도록 했기 때문에 Service에서는 값을 각각 받아오는 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;그런데 위 경우 dto에 필드가 많지 않아 직접 하나하나 인자로 넘겨주었지만 만약 필드가 많아지는 일반적인 경우 &lt;code&gt;controller &amp;lt;-&amp;gt; service&lt;/code&gt; 간의 dto를 새롭게 만들게 될 것이다. 즉, 아래와 같은 dto가 만들어진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Getter
@NoArgsConstructor
public class PlayParamDto {

    private String names;
    private int count;

    @Builder
    public PlayParamDto(String names, int count) {
        this.names = names;
        this.count = count;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 볼 수 있듯 PlayParamDto는 PlayRequestDto와 코드가 거의 동일해진다. 물론 api 스펙과 서비스에서 요구하는 인자가 다른 경우도 있겠지만 여러 리뷰어분들의 의견과 구글링을 통해 얻은 정보로는 &lt;strong&gt;서비스 로직과 컨트롤러 로직에서 사용하는 필드는 거의 동일&lt;/strong&gt;하다고 한다. 심지어 &lt;strong&gt;서비스 객체는 실무에서 많은 경우에 해당 도메인의 컨트롤러에서만 사용되는 것을 경험&lt;/strong&gt;하셨다고 한다. 그래서 오히려 서비스의 모듈화를 지나치게 일찍 고민해서 괜히 불필요한 dto(+중복 코드)를 마구 생성하고 있는게 아닌가 생각했다.&lt;/p&gt;
&lt;p&gt;심지어 이러한 중복 코드는 요청뿐만 아니라 응답 dto에서도 발생한다. 위 코드에서는 PlayResult가 일종의 controller &lt;-&gt; service 간의 dto 역할을 하고 있다.&lt;/p&gt;
&lt;p&gt;위와 같이 컨트롤러와 서비스의 필드가 동일하고 동일한 도메인에서만 사용되는 상황이 대부분이라면, 전체적으로는 dto를 service 계층에서까지 사용하도록 하고 &lt;strong&gt;정말로 서비스 계층의 모듈화가 필요한 상황이 되거나 서비스 로직과 컨트롤러 로직에서 필요로 하는 인자가 달라지는 경우&lt;/strong&gt; 해당 도메인에서만 service용 dto를 분리하는 방향으로 &lt;strong&gt;리팩토링&lt;/strong&gt; 하는게 오히려 비용이 적을 수 있겠다고 생각했다.&lt;/p&gt;
&lt;h3 id=&quot;service까지-사용&quot;&gt;Service까지 사용&lt;/h3&gt;
&lt;p&gt;그래서 그 다음엔 dto가 service에서 까지 참조되도록 구현했다. controller, service 코드는 각각 아래와 같이 변경될 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@RequiredArgsConstructor
@Controller
public class PlayController {
    private final PlayService playService;

    @PostMapping(&quot;/plays&quot;)
    public PlayResponseDto plays(@RequestBody PlayRequetDto requestDto) {
        return playService.play(requestDto);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;@Service
public class PlayService {
    public PlayResponseDto play(PlayRequestDto requestDto) {
        RacingCarGame racingCarGame = new RacingCarGame(requestDto.getNames(), requestDto.getCount());
        List&amp;lt;PlayResult&amp;gt; playResults = Collections.emptyList();

        while (!racingCarGame.isEnd()) {
            racingCarGame.play(movingStrategy);
            playResults = racingCarGame.getPlayResults();
        }

        // PlayResponseDto 생성 로직
        PlayResponseDto responseDto = ...;

        return responseDto;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;dto를-어떤-패키지에-둘지&quot;&gt;DTO를 어떤 패키지에 둘지&lt;/h2&gt;
&lt;p&gt;결론적으로 dto를 service 계층까지 사용하도록 구현이 되었다. 그런데 이렇게 되니 이제 그렇다면 dto는 controller 패키지에 있어야 되는지 service 패키지에 있어야 되는지가 고민이 되었다.&lt;/p&gt;
&lt;p&gt;기존에 controller에서만 사용할 때는 web/dto 패키지에 위치하면 딱 좋았는데 service에서 까지 사용하는 상태에서는 이렇게되면 service에서 상위 계층인 controller 계층에 의존하게 되므로 controller -&amp;gt; service -&amp;gt; domain 이라는 계층 간 의존 방향이 지켜지지 않게 된다.&lt;/p&gt;
&lt;p&gt;이럴 경우 dto를 service(비지니스) 패키지에 두면 controller에서 웹 요청을 처리하거나 응답할 때 하위 계층의 dto를 사용하게 되고 service 계층에서는 동일한 계층의 dto 객체를 사용하기 때문에 의존 방향이 반대가 되는 문제가 해결된다.&lt;/p&gt;
&lt;p&gt;처음에는 요청/응답을 처리하는 dto를 service에 두는게 어색했는데 관련하여 드린 질문에 대한 브라운(성현)님께서 주신 피드백을 인용하자면 &lt;code&gt;조금 바꾸어 생각해보면 웹 요청을 처리하는 dto라기 보다는 서비스의 비즈니스 기능을 호출할 때 쓰는 dto를 클라이언트에서부터 쓰고 있는거로도 볼 수 있는 것&lt;/code&gt; 이기 때문에 위에서 언급한 모든 내용들을 감안할 때 이러한 방향이 더 적합하다는 결론을 내렸다.&lt;/p&gt;
&lt;h2 id=&quot;계층-간-의존-방향을-왜-지켜야-할까&quot;&gt;계층 간 의존 방향을 왜 지켜야 할까&lt;/h2&gt;
&lt;p&gt;라는 근본적인 질문이 들었는데 지금으로썬 &lt;strong&gt;높은 결합도와 낮은 응집도를 유지하기 위해서&lt;/strong&gt; 인 것 같다. 계층 간 의존 방향이 섞여 있으면 각 계층이 서로 강하게 결합되고 이는 유지보수와 계층 별 단위 테스트를 어렵게 만든다. 또한 코드의 유연성과 확장성도 크게 떨어지게 된다.&lt;/p&gt;
&lt;p&gt;따라서 각 계층의 의존 방향을 한 방향으로 유지하는게 유지 보수성과 테스트 용이성을 고려한 좋은 설계라고 생각되고, 그런 의미에서 다시 한번 dto를 service 계층에 두는 것이 적합하다고 생각된다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot;&gt;결론&lt;/h2&gt;
&lt;p&gt;코드를 구현하면서 막연한 부분이 많았는데 생각해보면 개발에 좋은 방향은 있어도 단 하나의 정답은 없는 것이니까 그게 당연하다는 생각이 들었다. 계속해서 고민해나가면서 여러 가지 상황에 가장 적합한 방법들을 찾아나갈 필요가 있겠다고 생각했다. 지금 내린 결론도 더 많은 경험과 지식이 쌓이면 언제든지 바뀔 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;그래서 현재까지 내린 dto 사용 범위에 대한 결론은 아래와 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;기본적으로는 dto를 service 계층에 두고 client, controller, service에서 모두 사용하도록 하자.&lt;/li&gt;
&lt;li&gt;그러다가 controller와 service의 인자가 달라지거나 서비스를 모듈화해야 할 상황이 생기면 그때 service 용 dto를 생성해 리팩토링 하는 방식으로 개발 비용을 아끼고 중복 코드를 줄이자.&lt;/li&gt;
&lt;/ol&gt; &lt;/article&gt;</description>
      <category>개발 공부/Spring</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/103</guid>
      <comments>https://gmelon.dev/103#entry103comment</comments>
      <pubDate>Wed, 17 May 2023 11:25:02 +0900</pubDate>
    </item>
    <item>
      <title>[이펙티브 자바] Item 1 - 생성자 대신 정적 팩터리 메서드를 고려하라</title>
      <link>https://gmelon.dev/100</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;정적-팩토리-메서드란&quot;&gt;정적 팩토리 메서드란&lt;/h2&gt;
&lt;p&gt;정적 팩토리 메서드는 클래스에서 &lt;strong&gt;인스턴스를 생성&lt;/strong&gt;하는 용도로 생성자와 별도로 제공할 수 있는 또 다른 수단이다. 예를 들어 Boolean 클래스의 경우 아래와 같이 인스턴스를 생성할 수 있는 정적 팩토리 메서드를 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;정적-팩토리-메서드의-장점&quot;&gt;정적 팩토리 메서드의 장점&lt;/h3&gt;
&lt;p&gt;그럼 생성자 대신 정적 팩토리 메서드를 사용할 때 어떤 장점이 있을까? 책에서는 크게 5가지를 제시한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;이름을 가질 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;인스턴스를 생성하는 메서드가 이름을 가지게 되면 &lt;strong&gt;반환될 객체의 특성을 쉽고 정확하게 묘사&lt;/strong&gt;할 수 있다는 장점이 생긴다. 예를 들어 BigInteger의 probablePrime() 이라는 정적 팩토리 메서드는 &lt;strong&gt;소수인 BigInteger의 인스턴스를 반환한다&lt;/strong&gt; 라고 하는 반환 객체의 특성을 명확하게 드러낼 수 있다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;인스턴스 통제가 가능해진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;반복되는 요청에 동일한 (캐싱된) 객체를 반환하거나 아예 인스턴스화를 금지하는 등 정적 팩토리 메서드를 사용하면 생성자에서는 할 수 없는 &lt;strong&gt;인스턴스 통제&lt;/strong&gt;가 가능해진다. 예를 들어 아래 LotteryNumber 클래스의 경우 무조건 필드인 lotteryNumber 값이 1~45임이 보장되어 있기 때문에 해당 값들을 갖는 인스턴스를 static 초기화 블럭에서 미리 만들어두고 정적 팩토리 메서드를 통해 인스턴스를 요청하게 되면 매번 새로운 인스턴스를 반환하지 않고 이미 만들어진 값을 반환하는 식으로 동작하게 할 수 있다. 이러한 방식은 특히 인스턴스의 생성 비용이 클 경우에 성능을 향상시킬 수 있게 해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class LotteryNumber implements Comparable&amp;lt;LotteryNumber&amp;gt;{

    public static final int LOTTERY_NUM_MIN = 1;
    public static final int LOTTERY_NUM_MAX = 45;
    public static Map&amp;lt;Integer, LotteryNumber&amp;gt; lotteryNumbers = new HashMap&amp;lt;&amp;gt;();

    private final int lotteryNumber;

    // 클래스 로드 시 인스턴스를 미리 생성해둠
    static {
        for (int i = LOTTERY_NUM_MIN; i &amp;lt;= LOTTERY_NUM_MAX; i++) {
            lotteryNumbers.put(i, new LotteryNumber(i));
        }
    }

    private LotteryNumber(int number) {
        this.lotteryNumber = number;
    }

    // 정적 팩토리 메서드
    public static LotteryNumber lotteryNumber(int number) {
        // 미리 생성해둔 인스턴스 중에 찾아서 반환
        LotteryNumber lotteryNumber = lotteryNumbers.get(number);

        validate(lotteryNumber);
        return lotteryNumber;
    }

    private static void validate(LotteryNumber lotteryNumber) {
        if (lotteryNumber == null) {
            throw new IllegalArgumentException(&quot;로또 번호는 1과 45 사이의 정수여야 합니다.&quot;);
        }
    }

  // Object, Comparable 오버라이드 메서드 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;정해진 반환 타입의 하위 타입도 반환이 가능해진다.&lt;/li&gt;
&lt;li&gt;입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;정확하게 동일한 타입만 반환 가능한 생성자와 달리 정적 팩토리 메서드를 사용하면 &lt;strong&gt;반환 타입의 하위 타입&lt;/strong&gt;도 반환이 가능해진다. 이러한 특징은 구현 클래스를 공개하지 않고도 해당 객체를 반환할 수 있게 하여 API를 작게 유지할 수 있게 해준다. 예를 들어 아래 자바 API의 EnumSet은 아래와 같이 noneOf() 라는 정적 팩토리 메서드를 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static &amp;lt;E extends Enum&amp;lt;E&amp;gt;&amp;gt; EnumSet&amp;lt;E&amp;gt; noneOf(Class&amp;lt;E&amp;gt; elementType) {
    Enum&amp;lt;?&amp;gt;[] universe = getUniverse(elementType);
    if (universe == null)
        throw new ClassCastException(elementType + &quot; not an enum&quot;);

    if (universe.length &amp;lt;= 64)
        return new RegularEnumSet&amp;lt;&amp;gt;(elementType, universe);
    else
        return new JumboEnumSet&amp;lt;&amp;gt;(elementType, universe);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위에서 말한 특징 덕분에 EnumSet이 내부적으로 원소의 개수에 따라 RegularEnumSet 혹은 JumboEnumSet 라는 구현 클래스를 사용함에도 불구하고 반환 타입은 EnumSet으로 동일하게 가져갈 수 있다. 이를 받는 클라이언트 또한 (명시한 인터페이스대로 동작하는 객체를 얻을 것임을 알기에) 별도 문서를 찾아가며 구현 클래스가 무엇인지 찾아보지 않고 동일하게 해당 객체를 사용할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;또한 API를 공개한 이후 새로운 구현 클래스가 추가되거나 제거되어도 위와 같은 구조로 API를 설계했다면 클라이언트에게 알리지 않고 구현 클래스를 나중에 추가 / 삭제한다고 하더라도 전혀 문제가 되지 않는다. 클라이언트는 구현 클래스가 어떤 것인지 알 필요없지 단지 EnumSet의 하위 클래스이고 EnumSet의 규약(인터페이스)대로 해당 클래스가 동작하기만 하면 되기 때문이다. 정적 팩토리 메서드는 다형성을 이용해 이러한 유연성을 제공한다.&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;3번, 4번 장점과 통하는 부분이 있는 장점이라고 생각한다. 책에서는 이러한 유연함을 통해 &lt;strong&gt;서비스 제공자 프레임워크&lt;/strong&gt;를 만들 수 있다고 말한다. &lt;strong&gt;서비스 제공자 프레임워크&lt;/strong&gt;란 JDBC, JPA 와 같이 클라이언트에서 서비스의 구현체 (제공자)에 접근하는 것을 프레임워크가 통제하여 클라이언트를 특정 구현체로부터 분리해주는 것을 말한다.&lt;/p&gt;
&lt;p&gt;서비스 제공자 프레임워크는 1. 서비스 인터페이스 (구현체의 동작 정의), 2. 제공자 등록 API (제공자가 구현체를 등록), 3. 서비스 접근 API (클라이언트가 서비스의 인스턴스를 얻을 때 사용), 4. 서비스 제공자 인터페이스 (인터페이스의 인스턴스를 생성)로 구성되어 있다. JDBC의 경우 각각 1. Connection , 2. DriverManager.registerDriver, 3. DriverManager.getConnection, 4. Driver 가 그 역할을 수행한다.&lt;/p&gt;
&lt;p&gt;구현부와 기능부를 분리하고 연결하여 새로운 기능(구현체)을 프레임워크에 추가한다는 관점에서 &lt;strong&gt;브릿지패턴&lt;/strong&gt;이 적용되었다고도 볼 수 있는 것 같다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;이펙티브 자바 &lt;a href=&quot;https://github.com/2023-java-study/book-study/tree/main/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C_%EC%9E%90%EB%B0%94&quot;&gt;전체 아이템 목록&lt;/a&gt; (스터디 정리 레포지토리)&lt;/p&gt;
&lt;/blockquote&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/100</guid>
      <comments>https://gmelon.dev/100#entry100comment</comments>
      <pubDate>Fri, 28 Apr 2023 23:42:23 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - 완전탐색] 전력망을 둘로 나누기</title>
      <link>https://gmelon.dev/98</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/86971&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;p&gt;문제를 푸는 여러 가지 방법이 있을 것 같았는데 간단한 방법으로 문제를 풀었다.&lt;/p&gt;
&lt;p&gt;입력으로 간선 정보가 주어지므로 순회를 통해 하나씩 간선을 제외하고, 제외된 간선에 속한 노드를 각각 시작점으로 하여 그 시작점과 연결되어 있는 노드들의 합이 몇 개인지 카운트한다. 그리고 그렇게 카운트된 두 값의 차의 절댓값을 구한다. 이렇게 구한 절댓값의 최솟값을 계속해서 갱신해가면 모든 간선을 순회했을 때 가장 노드의 차이가 적을 때의 값을 구할 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 입력이 &lt;code&gt;[[1,3],[2,3],[3,4],[4,5],[4,6],[4,7],[7,8],[7,9]]&lt;/code&gt; 라고 하면, 가장 먼저 &lt;code&gt;[1, 3]&lt;/code&gt; 간선이 선택될 것이고 &lt;strong&gt;(그래프에서 해당 간선을 제외)&lt;/strong&gt; 1번과 3번 노드를 분리해놓고 1번과 3번으로부터 시작되는 트리에 속한 노드들을 각각 구한다. 트리에 속한 노드를 구하는 방법은 여러 가지가 있겠지만 간단하게 dfs를 사용했다. 그리고 그 값의 차의 절댓값을 구하게 된다.&lt;/p&gt;
&lt;p&gt;아래 그림은 순회를 계속하던 중 &lt;code&gt;[4, 7]&lt;/code&gt; 간선이 제외된 상태에서의 트리이며 이 경우 6, 3이라는 값이 각각 구해지고 두 값의 차의 절댓값은 3이 나오게 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dM6ka9/btr92qOyOuf/yfGl2AaKQhxpqB2BmGKo8k/img.png&quot; alt=&quot;image-20230413145228290&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이 과정을 전체 간선에 대해 반복하면 답을 쉽게 구할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;코드&quot;&gt;코드&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;import java.util.*;

class Solution {

    public int solution(int n, int[][] wires) {
        int answer = Integer.MAX_VALUE;

        // 전체 그래프 생성
        Map&amp;lt;Integer, List&amp;lt;Integer&amp;gt;&amp;gt; graph = new HashMap&amp;lt;&amp;gt;();
        for (int[] wire : wires) {
            int nodeA = wire[0];
            int nodeB = wire[1];

            // 양방향으로 등록
            if (graph.get(nodeA) == null) {
                graph.put(nodeA, new ArrayList&amp;lt;&amp;gt;());
            }
            graph.get(nodeA).add(nodeB);

            if (graph.get(nodeB) == null) {
                graph.put(nodeB, new ArrayList&amp;lt;&amp;gt;());
            }
            graph.get(nodeB).add(nodeA);
        }

        // 하나의 간선씩 제외하며 dfs 순회하여 최소 값 비교하기
        for (int[] wire : wires) {
            // 간선 제거
            graph.get(wire[0]).remove(Integer.valueOf(wire[1]));
            graph.get(wire[1]).remove(Integer.valueOf(wire[0]));

            // 각 노드를 시작으로 하여 각 트리의 노드 개수 확인
            int diff = dfs(n, graph, wire[0]) - dfs(n, graph, wire[1]);
            // diff의 절대값의 최소값으로 answer를 갱신
            answer = Math.min(answer, Math.abs(diff));

            // 간선 다시 추가
            graph.get(wire[0]).add(wire[1]);
            graph.get(wire[1]).add(wire[0]);
        }

        return answer;
    }

    // 노드에 연결된 노드 수를 카운트하기 위한 dfs 메서드
    public int dfs(final int n, final Map&amp;lt;Integer, List&amp;lt;Integer&amp;gt;&amp;gt; graph, int startNode) {
        int count = 0;
        Stack&amp;lt;Integer&amp;gt; stack = new Stack&amp;lt;&amp;gt;();
        boolean[] visited = new boolean[n + 1];

        stack.push(startNode);

        while(!stack.isEmpty()) {
            Integer current = stack.pop();
            count++;
            visited[current] = true;

            if (graph.get(current) == null) {
                continue;
            }

            for (Integer neighbor : graph.get(current)) {
                if (!visited[neighbor]) {
                    stack.push(neighbor);
                }
            }
        }

        return count;
    }

}&lt;/code&gt;&lt;/pre&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/98</guid>
      <comments>https://gmelon.dev/98#entry98comment</comments>
      <pubDate>Thu, 13 Apr 2023 14:53:24 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 부록 - 추상화 기법</title>
      <link>https://gmelon.dev/97</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;추상화-기법&quot;&gt;추상화 기법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;추상화 -&amp;gt; &lt;strong&gt;도메인의 복잡성을 단순화&lt;/strong&gt;하고 직관적 멘탈 모델을 만드는 데 사용할 수 있는 가장 기본적인 인지 수단&lt;ul&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;/ul&gt;
&lt;h2 id=&quot;1-분류와-인스턴스화&quot;&gt;1. 분류와 인스턴스화&lt;/h2&gt;
&lt;h3 id=&quot;개념과-범주&quot;&gt;개념과 범주&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체를 분류하고 &lt;strong&gt;범주&lt;/strong&gt;로 묶는 것은 객체들의 특정 집합에 &lt;strong&gt;공통의 개념&lt;/strong&gt;을 적용하는 것을 의미함&lt;/li&gt;
&lt;li&gt;분류 -&amp;gt; 세상에 존재하는 객체에 개념(타입)을 적용하는 과정&lt;ul&gt;
&lt;li&gt;즉, 객체를 타입(개념)과 연관시키는 것&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;타입&quot;&gt;타입&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체가 타입에 속하는지 검증하기 위해 필요한 타입의 정의 3가지&lt;ul&gt;
&lt;li&gt;심볼(타입의 이름), 내연(타입의 정의), 외연(타입에 속하는 객체들의 집합)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;외연과-집합&quot;&gt;외연과 집합&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;외연은 집합과 동일한 개념임&lt;/li&gt;
&lt;li&gt;객체는 여러 집합(외연)에 동시에 포함될 수 있다(다중 분류)&lt;ul&gt;
&lt;li&gt;또한 시간에 따라 타입이 바뀌는 동적 분류도 존재&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;하지만 대부분의 객체지향 언어는 단일 분류 + 정적 분류만 지원&lt;/strong&gt; 하므로 이를 개념적 수준을 넘어 구현으로 옮기기는 어렵다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;따라서 현실적으로는 도메인 모델을 작성할 땐 개념적으로 다중 + 동적 분류 관점에서 모델을 작성하고 &lt;strong&gt;이후에 구현에 적합하도록 객체들의 범주를 수정&lt;/strong&gt;하는 방법을 제안하고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;클래스&quot;&gt;클래스&lt;/h3&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;li&gt;클래스는 객체가 공유하는 &lt;strong&gt;본질적인 속성&lt;/strong&gt;을 정의한다&lt;ul&gt;
&lt;li&gt;우연적(본질적이지 않은) 속성은 표현할 수 없다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;2-일반화와-특수화&quot;&gt;2. 일반화와 특수화&lt;/h2&gt;
&lt;h3 id=&quot;범주의-계층&quot;&gt;범주의 계층&lt;/h3&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 id=&quot;서브타입&quot;&gt;서브타입&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;서브타입은 슈퍼타입의 본질적인 속성을 모두 포함하기 때문에 계층에 속하는 모든 서브타입들이 슈퍼타입의 속성을 공유한다는 것을 쉽게 예상할 수 있다.&lt;/li&gt;
&lt;li&gt;어떤 타입이 다른 타입의 서브 타입이 되기 위해선 아래 두 가지 규칙을 만족해야 한다&lt;ol&gt;
&lt;li&gt;100% 규칙 - 슈퍼타입의 정의가 100% 서브타입에 적용되어야 한다. 서브타입의 속성과 연관관계가 슈퍼타입과 100% 일치해야 한다 (내연의 관점)&lt;/li&gt;
&lt;li&gt;Is-a 규칙 - 서브타입이 슈퍼타입의 부분집합이어야 한다 (외연의 관점)&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;상속&quot;&gt;상속&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;일반화 - 특수화 관계를 구현하는 가장 일반적인 방법&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;하지만, 모든 상속 관계가 일반화 관계는 아니다&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;서브타입이 슈퍼타입에 &lt;strong&gt;순응&lt;/strong&gt;해야만 일반화 관계가 성립한다 (대체 가능성과 연관)&lt;ul&gt;
&lt;li&gt;구조적인 순응 - 슈퍼타입의 속성과 연관관계를 서브타입도 동일하게 가질 것 (100% 규칙)&lt;/li&gt;
&lt;li&gt;행위적인 순응 - 리스코프 치환 원칙을 만족할 것. 즉, 동일한 메서드에 대해 동일한 (행위적인 관점에서) 응답을 가질 것.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;일반화 관계가 성립할 땐 &lt;strong&gt;서브 타이핑&lt;/strong&gt;이라 하고, 그렇지 않을 땐 &lt;strong&gt;서브 클래싱&lt;/strong&gt;이라 칭한다&lt;ul&gt;
&lt;li&gt;서브 클래싱은 단순 코드 중복 방지나 공통 코드 재활용 등으로 상속이 활용되는 경우를 말한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가능한 모든 상속 관계가 서브 파이핑의 대체 가능성을 준수하도록 해야 코드의 유연성 &amp; 재사용성이 높아진다&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;3-집합과-분해&quot;&gt;3. 집합과 분해&lt;/h2&gt;
&lt;h3 id=&quot;계층적인-복잡성&quot;&gt;계층적인 복잡성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;집합&lt;/strong&gt;의 가치는 많은 수의 사물들의 형상을 하나의 단위로 다룸으로써 복잡성을 줄일 수 있다는 데 있다&lt;ul&gt;
&lt;li&gt;필요한 시점에는 전체를 분해하여 부분을 새로운 전체로 다룰 수도 있다&lt;/li&gt;
&lt;li&gt;전체와 부분 간의 일관된 계층 구조는 &lt;strong&gt;재귀적 설계&lt;/strong&gt;를 가능하게 한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;집합은 추상화인 동시에 캡슐화 매커니즘이다&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;한번에 다뤄야 하는 요소의 수를 감소&lt;/strong&gt;시킴으로써 인지 과부하를 방지한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;합성-관계&quot;&gt;합성 관계&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체와 객체 사이의 전체-부분 관계를 구현하기 위해 &lt;strong&gt;합성 관계&lt;/strong&gt;를 사용&lt;ul&gt;
&lt;li&gt;ex) 주문 - 주문 상품 (주문 상품은 주문과 별개로 존재할 수 없음)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;주문 상품은 주문 안에 속하므로 주문과 다른 객체와 관계를 맺을 때 &lt;strong&gt;다른 객체는 주문 상품을 고려하지 않고 주문만 존재하는 것처럼 생각&lt;/strong&gt;할 수 있다 -&amp;gt; 복잡성 완화&lt;/li&gt;
&lt;li&gt;포함되지 않고 그냥 관계가 존재할 때는 이를 &lt;strong&gt;연관 관계&lt;/strong&gt;라고 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;패키지&quot;&gt;패키지&lt;/h3&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;hr /&gt;
&lt;p&gt;이 게시글은 스터디에서 [ &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001628109&quot;&gt;객체지향의 사실과 오해 / 조영호&lt;/a&gt; ] 책을 읽고 중요한 내용을 잊지 않기 위해 정리한 게시글입니다. 요약 및 생략된 내용이 많고 제가 이해한 대로 다시 정리한 내용이라 보다 자세하고 정확한 설명은 책 구매를 권장합니다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/97</guid>
      <comments>https://gmelon.dev/97#entry97comment</comments>
      <pubDate>Mon, 10 Apr 2023 18:38:13 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 전화번호 목록</title>
      <link>https://gmelon.dev/95</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42577&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;p&gt;예를 들어  &lt;code&gt;phone_book&lt;/code&gt; 배열이 &lt;code&gt;[&quot;12&quot;,&quot;123&quot;,&quot;1235&quot;,&quot;567&quot;,&quot;88&quot;]&lt;/code&gt; 처럼 주어졌을 때 &lt;code&gt;1235&lt;/code&gt;에 &lt;code&gt;12&lt;/code&gt;가 포함되므로 접두사가 되며 &lt;code&gt;false&lt;/code&gt;가 정답이 된다. 이를 단순하게 확인하려면 모든 원소에 대하여 모든 다른 원소와 비교하면 문제는 해결되지만 시간복잡도가 &lt;code&gt;O(n^2)&lt;/code&gt;이 되어 효율성 테스트에서 오답이 나오게 된다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;O(n)&lt;/code&gt;에 문제를 해결하려면 한번만 순회하면서 답을 찾아야 한다. 이를 위해서는 각각의 자릿수에 대하여 더 작은 수가 앞에 오도록 정렬을 해주면 된다. 예를 들어 &lt;code&gt;&quot;12&quot;&lt;/code&gt;와 &lt;code&gt;&quot;123&quot;&lt;/code&gt; 이 있다면 &lt;code&gt;&quot;12&quot;, &quot;123&quot;&lt;/code&gt;으로 정렬을 해야 하고 &lt;code&gt;&quot;1&quot;&lt;/code&gt;과 &lt;code&gt;&quot;123&quot;&lt;/code&gt; 이 있다면 &lt;code&gt;&quot;1&quot;, &quot;123&quot;&lt;/code&gt;으로, &lt;code&gt;&quot;2&quot;&lt;/code&gt;와 &lt;code&gt;&quot;24&quot;&lt;/code&gt;와 &lt;code&gt;&quot;134&quot;&lt;/code&gt;이 있다면 &lt;code&gt;&quot;134&quot;, &quot;2&quot;, &quot;24&quot;&lt;/code&gt;로 정렬을 수행해야 한다. 이렇게 하면, 바로 직전의 수가 다음 수에 포함되는지 확인하는 것만으로 전체 수간에 접두사 관계가 존재하는지를 확인할 수 있게 된다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;맨 앞에 &lt;code&gt;&quot;1&quot;&lt;/code&gt; 이 있고 한참 뒤에 있는 &lt;code&gt;&quot;123&quot;&lt;/code&gt;의 경우 맨 앞의 &lt;code&gt;&quot;1&quot;&lt;/code&gt;이 접두사로 인식되지 못하는 것 아닌가? 하는 생각이 들었지만 생각해보면 &lt;code&gt;&quot;1&quot;&lt;/code&gt;과 &lt;code&gt;&quot;123&quot;&lt;/code&gt; 사이에 아무런 숫자가 없다면 붙어있으므로 당연히 접두사로 인식될 것이고, 사이에 숫자가 있다면 무조건 &lt;code&gt;&quot;1&quot;&lt;/code&gt;을 포함하는 숫자일 것이므로 이러한 문제는 발생하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;자바에서 문자열로 이루어진 배열을 정렬하면 앞서 말한 것처럼 정렬을 수행해준다. 따라서 정렬을 수행한 후 String 클래스의 &lt;code&gt;startsWith()&lt;/code&gt; 메서드를 통해 현재 문자열이 직전의 문자열로 시작되는지를 확인하면 된다.&lt;/p&gt;
&lt;p&gt;전체 코드는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;import java.util.*;

class Solution {
    public boolean solution(String[] phone_book) {
        // &quot;1&quot;, &quot;1xx&quot;, &quot;1xxx&quot;, &quot;2&quot;, &quot;24&quot;, &quot;59&quot;, &quot;599&quot;, ...
        Arrays.sort(phone_book);       

        for (int i = 1 ; i &amp;lt; phone_book.length ; i++) {
            // 현재 문자열이 직전 문자열로 시작하는지 확인
            if (phone_book[i].startsWith(phone_book[i - 1])) {
                return false;
            }
        }
        // 접두사 관계가 없다면 true 반환
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/95</guid>
      <comments>https://gmelon.dev/95#entry95comment</comments>
      <pubDate>Thu, 6 Apr 2023 01:15:58 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 7장 - 함게 모으기</title>
      <link>https://gmelon.dev/94</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;객체지향-설계-안에-존재하는-세-가지-상호-연관된-관점&quot;&gt;객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점&lt;/h2&gt;
&lt;h3 id=&quot;개념-관점&quot;&gt;개념 관점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현&lt;/li&gt;
&lt;li&gt;실제 도메인의 규칙과 제약을 최대한 유사하게 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;명세-관점&quot;&gt;명세 관점&lt;/h3&gt;
&lt;ul&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;/ul&gt;
&lt;h3 id=&quot;구현-관점&quot;&gt;구현 관점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;실제 작업을 수행하는 코드와 연관&lt;/li&gt;
&lt;li&gt;객체의 책임을 '어떻게' 수행할 것인가에 초점을 두고 필요한 속성과 메서드를 클래스에 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;위 3가지 관점은 시간 순이 아니며, &lt;strong&gt;동일한 클래스를 세 가지 다른 방향에서 바라보는 것을 의미&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;하나의 클래스 안에 세 가지 관점을 모두 포함하면서도 각 관점에 대응되는 요소를 명확하고 깔끔하게 드러낼 수 있어야 한다!&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;도메인-모델---최종-코드-구현-과정-요약&quot;&gt;도메인 모델 -&amp;gt; 최종 코드 구현 과정 (요약)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;구현하고자 하는 시스템에 속한 객체들을 식별해 &lt;strong&gt;도메인 모델을 설계&lt;/strong&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;strong&gt;도메인 모델에서 해당 메시지를 수신하기에 적합한 객체를 찾아낸다&lt;/strong&gt; (어떤 객체를 &lt;strong&gt;은유&lt;/strong&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;code&gt;3단계&lt;/code&gt;에서 &lt;strong&gt;설계된 인터페이스는 구현하는 과정에서 대부분 변경되고 새롭게 추가&lt;/strong&gt;된다. 따라서 협력을 구상하는 단계에 시간을 너무 투자하지 말고 협력 구조가 떠오르거나 설계가 막힌다면 최대한 빨리 구현 단계로 넘어와 &lt;strong&gt;코드를 통해 설계에 대한 피드백&lt;/strong&gt;을 받아야 한다.&lt;/li&gt;
&lt;li&gt;객체의 속성 또한, 구현 단계에 와서야 결정된다. (+ 인터페이스 설계 단계에서는 결정되지 않아야 한다.)&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;이 게시글은 스터디에서 [ &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001628109&quot;&gt;객체지향의 사실과 오해 / 조영호&lt;/a&gt; ] 책을 읽고 중요한 내용을 잊지 않기 위해 정리한 게시글입니다. 요약 및 생략된 내용이 많고 제가 이해한 대로 다시 정리한 내용이라 보다 자세하고 정확한 설명은 책 구매를 권장합니다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/94</guid>
      <comments>https://gmelon.dev/94#entry94comment</comments>
      <pubDate>Wed, 5 Apr 2023 18:21:03 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 6장 - 객체 지도</title>
      <link>https://gmelon.dev/93</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;blockquote&gt;
  &lt;p&gt;기능을 중심으로 구조를 종속시키는 적근법은 범용적이지 않고 재사용이 불가능하며 변경에 취약한 모델을 낳게 된다. 이와 달리 안정적인 구조를 중심으로 기능을 종속시키는 접근법은 범용적이고 재사용 가능하며 변경에 유연하게 대처할 수 있는 모델을 만든다. (중략) 자주 변경되는 기능이 아니라 안정적인 구조를 따라 역할, 책임, 협력을 구성하라. (p.180)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;기능-설계-vs-구조-설계&quot;&gt;기능 설계 vs 구조 설계&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;성공적인 소프트웨어 -&amp;gt; 사용자가 원하는 새로운 기능을 빠르고 안정적으로 추가할 수 있다&lt;ul&gt;
&lt;li&gt;이는 &lt;strong&gt;훌륭한 구조가 뒷받침&lt;/strong&gt;되어 있기에 가능한 것&lt;/li&gt;
&lt;li&gt;비록 최종 사용자들은 내부 구조를 볼 수 없지만, 좋은 설계는 사용자의 요구사항을 빠르게 반영할 수 있기에 중요하다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;설계라는 행위를 중요하게 만드는 것은 변경에 대한 필요성이다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;설계를 하는 목적은 나중에 설계하는 것을 허용하는 것&lt;/strong&gt;이며 설계의 목표는 변경의 비용을 낮추는 것&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;변경을 예측하지 말고 변경을 수용할 수 있는 선택의 여지를 설계에 마련해두라&lt;ul&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;strong&gt;객체를 이용해 지도를 만들고, 기능이 객체로 만들어진 길을 따라 자연스럽게 흘러가게 하라&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;두-가지-재료--기능과-구조&quot;&gt;두 가지 재료 : 기능과 구조&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;구조 : 이해관계자들이 도메인(domain)에 관해 생각하는 개념과 개념들 간의 관계로 표현&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;/ul&gt;
&lt;h2 id=&quot;안정적-재료--구조&quot;&gt;안정적 재료 : 구조&lt;/h2&gt;
&lt;h3 id=&quot;도메인-모델&quot;&gt;도메인 모델&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;도메인 : 사용자가 프로그램을 사용하는 대상 분야&lt;/li&gt;
&lt;li&gt;도메인 모델 : 도메인에 대한 지식을 (문제에 관련된 측면만) 선택적으로 단순화하고 의식적으로 구조화한 형태&lt;ul&gt;
&lt;li&gt;도메인 모델은 단순 다이어그램이 아님. 이해관계자들이 바라보는 &lt;strong&gt;멘탈 모델&lt;/strong&gt; (사람들이 자신과 상호작용하는 사물, 사람, 환경 등에 대해 갖는 모형으로 현상을 이해하고 거기에 반응하기 위해 각자가 구축함) 이다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;애플리케이션은 도메인 모델을 기반으로 설계되어야 한다&lt;ul&gt;
&lt;li&gt;즉, 소프트웨어의 코드는 &lt;strong&gt;사용자들이 도메인을 바라보는 관점&lt;/strong&gt;과 &lt;strong&gt;설계자가 시스템의 구조를 바라보는 관점&lt;/strong&gt;을 기반으로 설계되어야 한다&lt;/li&gt;
&lt;li&gt;객체지향을 사용하면 도메인 모델을 사용자들의 &lt;strong&gt;이해&lt;/strong&gt;와 최대한 유사하게 코드로 구조화할 수 있다 (표현적 차이)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;표현적-차이&quot;&gt;표현적 차이&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체지향은 현실 객체에 대한 추상화가 아니라 &lt;strong&gt;은유를 기반으로 재창조한 것&lt;/strong&gt;이다&lt;ul&gt;
&lt;li&gt;즉, 소프트웨어 객체는 현실 객체의 특성을 기반으로 구축되며 이때의 의미적 거리를 &lt;strong&gt;표현적 차이&lt;/strong&gt;라고 한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;우리가 은유를 통해 투영해야 하는 대상은 &lt;strong&gt;사용자가 도메인에 대해 생각하는 개념&lt;/strong&gt; (도메인 모델)&lt;ul&gt;
&lt;li&gt;코드의 구조가 도메인의 구조를 반영하면 소프트웨어가 이해하고 수정하기 쉬워진다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;불안정한-기능을-담는-안정적인-도메인-모델&quot;&gt;불안정한 기능을 담는 안정적인 도메인 모델&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;도메인 구조는 상대적으로 안정적 -&amp;gt; 도메인 모델을 기반으로 코드 작성 이유&lt;ul&gt;
&lt;li&gt;사용자 모델이 반영된 도메인 구조는 비교적 변경될 확률이 적다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도메인 모델은 기능을 구현할 때 참조할 수 있는 궁극적인 지도&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;도메인 모델은 비즈니스의 개념과 정책을 반영하는 안정적 구조를 제공&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;불안정한-재료--기능&quot;&gt;불안정한 재료 : 기능&lt;/h2&gt;
&lt;h3 id=&quot;유스케이스&quot;&gt;유스케이스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;유스케이스 : 사용자의 목표를 달성하기 위해 사용자와 시스템 간에 이뤄지는 상호작용의 흐름을 &lt;strong&gt;텍스트&lt;/strong&gt;로 정리한 것&lt;ul&gt;
&lt;li&gt;사용자의 &lt;strong&gt;목표&lt;/strong&gt;를 중심으로 시스템의 기능 요구사항들을 &lt;strong&gt;이야기(시나리오) 형태로 묶을 수 있다는 가치&lt;/strong&gt;가 있다&lt;/li&gt;
&lt;li&gt;훌륭한 기능적 요구사항을 얻기 위해서는 유스케이스(상호작용)를 중심으로 시스템을 바라봐야 한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;유스케이스의-특성&quot;&gt;유스케이스의 특성&lt;/h3&gt;
&lt;ul&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;단순 feature의 목록이 아니라, 사용자와의 상호작용 속에서 feature들을 포함하는 이야기(시스템의 기능)를 제공&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;본질적인 유스케이스&lt;/strong&gt; - 특정 feature를 사용하기 위한 &lt;strong&gt;사용자 인터페이스 등 자주 변경되는 요소는 배제&lt;/strong&gt;하고 시스템의 행위에 초점을 맞춘다&lt;/li&gt;
&lt;li&gt;내부 설계와 관련된 정보를 포함하지 않는다&lt;ul&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;/ul&gt;
&lt;h2 id=&quot;기능과-구조의-통합&quot;&gt;기능과 구조의 통합&lt;/h2&gt;
&lt;h3 id=&quot;도메인-모델-유스케이스-책임-주도-설계&quot;&gt;도메인 모델, 유스케이스, 책임-주도 설계&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;유스케이스에 정리된 시스템의 기능(책임)&lt;/strong&gt;을 &lt;strong&gt;도메인 모델을 기반&lt;/strong&gt;으로 한 &lt;strong&gt;객체들의 책임으로 분배&lt;/strong&gt;해야 함&lt;ul&gt;
&lt;li&gt;시스템의 책임을 작은 객체의 책임으로 분배할 때 &lt;strong&gt;도메인 모델에 포함된 개념을 은유하는 소프트웨어 객체를 선택&lt;/strong&gt;해야 함&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;책임-주도 설계는 유스케이스로부터 첫 번째 메시지와 사용자가 달성하려는 목표를, 도메인 모델로부터 기능을 수용하기 위해 은유할 수 있는 안정적인 구조를 제공받아 실제로 동작하는 객체들의 협력 공동체를 창조한다. (p.199)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;설계-방법-예시&quot;&gt;설계 방법 예시&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;책 p.199 ~ p.201 참고&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;시스템의 책임이 있을 때 메시지를 받을 객체(책임 수행을 시작)를 먼저 선택하고 (도메인 모델 참조)&lt;ul&gt;
&lt;li&gt;그 객체가 다른 객체에 전송할 메시지를 식별한 후&lt;/li&gt;
&lt;li&gt;다시 그 메시지를 받을 객체를 선택 -&amp;gt; &lt;strong&gt;협력 관계를 창조&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;실세계에서는 수동적인 존재라고 하더라도 소프트웨어 객체로 구현될 때는 스스로 판단하고 행동하는 자율적인 존재로 변한다. (p.201)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;책임 할당의 기본 원칙 -&amp;gt; 책임을 수행하는 데 필요한 정보를 가진 객체에게 그 책임을 할당&lt;ul&gt;
&lt;li&gt;상태와 행동을 캡슐화하게 됨&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;기능-변경-발생-시&quot;&gt;기능 변경 발생 시&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;비즈니스 정책이나 규칙이 크게 변경되지 않는 한, 시스템의 기능이 변경되더라도 객체 간의 관계를 일정하게 유지됨&lt;ul&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;strong&gt;책임과 객체의 대응 관계만 수정&lt;/strong&gt;됨&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;사람들이 동일한 용어와 동일한 개념을 이용해 의사소통하고 &lt;strong&gt;코드로부터 도메인 모델을 유추&lt;/strong&gt;할 수 있게 하는 것이 &lt;strong&gt;도메인 모델의 진정한 목표&lt;/strong&gt;다. (중략) &lt;strong&gt;도메인 모델과 코드를 밀접하게 연관&lt;/strong&gt;시키기 위해 노력하라. 그것이 유지보수하기 쉽고 유연한 객체지향 시스템을 만드는 첫걸음이 될 것이다. (p.206)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;질문&quot;&gt;질문&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;p.183 무슨 말이지ㅜ&lt;/li&gt;
&lt;li&gt;p.205 - 도메인을 프로그래밍하기 위한 기법과 도메인을 모델링하기 위해 사용하는 기법이 같다는게 무슨 의미인지&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;이 게시글은 스터디에서 [ &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001628109&quot;&gt;객체지향의 사실과 오해 / 조영호&lt;/a&gt; ] 책을 읽고 중요한 내용을 잊지 않기 위해 정리한 게시글입니다. 요약 및 생략된 내용이 많고 제가 이해한 대로 다시 정리한 내용이라 보다 자세하고 정확한 설명은 책 구매를 권장합니다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/93</guid>
      <comments>https://gmelon.dev/93#entry93comment</comments>
      <pubDate>Tue, 4 Apr 2023 16:09:14 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 5장 - 책임과 메시지</title>
      <link>https://gmelon.dev/92</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;자율적인-책임&quot;&gt;자율적인 책임&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;자율적 객체 -&amp;gt; 스스로의 의지와 판단에 따라 각자 맡은 책임을 수행하는 객체&lt;/li&gt;
&lt;li&gt;객체가 &lt;strong&gt;책임을 자율적으로 수행&lt;/strong&gt;하기 위해선 -&amp;gt; 객체에게 &lt;strong&gt;할당되는 책임이 자율적이어야&lt;/strong&gt; 한다&lt;ul&gt;
&lt;li&gt;상세한 수준의 책임은 협력의 최종 목표는 만족시킬지 몰라도 객체가 가져야 하는 선택의 자유를 크게 훼손한다&lt;/li&gt;
&lt;li&gt;이런 경우, 자신의 판단이 아닌 &lt;strong&gt;외부의 명령에 의존&lt;/strong&gt;하게 된다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;책임은 자율성을 보장할 수 있도록 &lt;strong&gt;포괄적이고 추상적이면서도 할 일을 명확하게 명시&lt;/strong&gt;하는 것이어야 한다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;'어떻게'가 아닌, '무엇'을 책임으로 사용하라&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;너무-추상적인-책임&quot;&gt;너무 추상적인 책임&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;단, 협력의 의도를 명확하게 표현하지 못할 정도로 추상적인 책임 역시 문제다&lt;ul&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;/ul&gt;
&lt;h3 id=&quot;책임을-자극하는-메시지&quot;&gt;책임을 자극하는 메시지&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;책임&lt;/strong&gt;은 &lt;strong&gt;어떤 행동을 수행한다&lt;/strong&gt;는 의미가 포함돼 있다&lt;ul&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;/ul&gt;
&lt;h2 id=&quot;메시지와-메서드&quot;&gt;메시지와 메서드&lt;/h2&gt;
&lt;h3 id=&quot;메시지&quot;&gt;메시지&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;메시지 - 메시지 이름 + 인자(추가 정보)로 구성 (&lt;code&gt;메시지이름(인자)&lt;/code&gt;의 형태)&lt;/li&gt;
&lt;li&gt;메시지 전송 - 객체가 다른 객체에 접근할 수 있는 유일한 방법&lt;ul&gt;
&lt;li&gt;메시지 전송 - 수신자 &amp;amp; 메시지의 조합&lt;/li&gt;
&lt;li&gt;즉, 메시지 전송은 수신자 &amp;amp; 메시지 이름 &amp;amp; 인자의 조합 (&lt;code&gt;수신자.메시지이름(인자)&lt;/code&gt; 의 형태)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;객체가 메시지를 &lt;strong&gt;수신할 수 있다&lt;/strong&gt;는 것 -&amp;gt; 객체가 메시지에 해당하는 &lt;strong&gt;책임을 수행할 수 있다&lt;/strong&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;/ul&gt;
&lt;h3 id=&quot;메서드&quot;&gt;메서드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;메서드 -&amp;gt; &lt;strong&gt;메시지를 처리&lt;/strong&gt;하기 위해 내부적으로 선택하는 방법&lt;/li&gt;
&lt;li&gt;메시지는 '어떻게' 가 아닌 처리되기 바라는 '무엇'을 명시하며 메서드는 수신자에 의해 내부적으로 결정되어 선택된다&lt;ul&gt;
&lt;li&gt;런타임에 실행할 메서드를 선택할 수 있는 것은 객체지향 언어의 차별점 중 하나이다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;다형성&quot;&gt;다형성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;다형성 -&amp;gt; 서로 다른 타입에 속하는 객체들이 &lt;strong&gt;동일 메시지&lt;/strong&gt;를 수신했을 때 &lt;strong&gt;서로 다른 메서드&lt;/strong&gt;를 통해 메시지를 처리하는 매커니즘&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;하나의 메시지와 하나 이상의 메서드 사이의 &lt;strong&gt;관계&lt;/strong&gt;로 볼 수도 있다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;각 객체는 자율성을 가졌기 때문에 각자 나름의 방법대로 책임을 수행하지만, &lt;strong&gt;메시지를 보낸 객체 입장에서 원하는 결과를 얻을 수 있다는 것은 달라지지 않는다&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&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;strong&gt;캡슐화&lt;/strong&gt;한다고 할 수 있다 -&amp;gt; 클라이언트는 메시지를 받는 객체의 타입을 몰라도 된다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체지향 패러다임이 강력한 이유는 다형성을 이용해 협력을 유연하게 만들 수 있기 때문이라는 점을 기억하라. (p.152)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;송신자가-수신자에-대해-매우-적은-정보만-알고도-협력이-가능하다는-점이-설계에-미치는-영향&quot;&gt;'송신자가 수신자에 대해 매우 적은 정보만 알고도 협력이 가능하다'는 점이 설계에 미치는 영향&lt;/h4&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;blockquote&gt;
  &lt;p&gt;이 모든 것은 다형성을 지탱하는 메시지가 존재하기 때문에 가능한 것이다. &lt;strong&gt;메시지는 송신자와 수신자 사이의 결합도를 낮춤&lt;/strong&gt;으로써 설계를 유연하고, 확장 가능하고, 재사용 가능하게 만든다. (p.153)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;메시지를-따라라&quot;&gt;메시지를 따라라&lt;/h2&gt;
&lt;h3 id=&quot;객체지향의-핵심-메시지&quot;&gt;객체지향의 핵심, 메시지&lt;/h3&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체지향 애플리케이션의 중심 사상은 연쇄적으로 메시지를 전송하고 수신하는 객체들 사이의 협력 관계를 기반으로 사용자에게 유용한 기능을 제공하는 것 (p.154)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;클래스는 단지 &lt;strong&gt;동적인 객체들의 특성과 행위를 정적인 텍스트로 표현&lt;/strong&gt;하기 위해 사용할 수 있는 추상화 도구일 뿐&lt;ul&gt;
&lt;li&gt;클래스를 중심에 두는 설계는 유연하지 못하고 확장하기 어렵다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;또한, 개별적인 객체가 아니라 메시지를 통한 &lt;strong&gt;객체들간의 커뮤니케이션&lt;/strong&gt;에 초점을 두어야 함&lt;ul&gt;
&lt;li&gt;결국 객체를 이용하는 중요한 이유는 해당 객체가 다른 객체가 필요로 하는 행위를 제공하기 때문&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;객체가 메시지를 선택하는 것이 아니라 &lt;strong&gt;메시지가 객체를 선택&lt;/strong&gt;하게 해야 한다&lt;ul&gt;
&lt;li&gt;그러려면, &lt;strong&gt;메시지를 중심으로 협력을 설계&lt;/strong&gt;해야 한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;책임-주도-설계&quot;&gt;책임-주도 설계&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;책임을 완수하기 위해 협력하는 객체들을 이용해 시스템을 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;시스템이 수행할 책임을 식별&lt;/li&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;li&gt;&lt;strong&gt;책임이 완수&lt;/strong&gt;될 때 까지 3, 4번 과정을 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;-&amp;gt; 결과적으로 &lt;strong&gt;메시지가 수신자의 책임을 결정&lt;/strong&gt;한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;whatwho-사이클&quot;&gt;What/Who 사이클&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;어떤 행위(What, 메시지)가 필요한지를 먼저 결정한 후에 이 행위를 수행할 객체(Who)를 결정하는 것&lt;ul&gt;
&lt;li&gt;책임-주도 설계의 핵심 아이디어를 명확하게 표현하는 용어&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;어떤 객체가 필요한지를 생각하지 말고, 어떤 메시지가 필요한지를 먼저 고민하라&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;책임-주도 설계의 관점에서는 &lt;strong&gt;어떤 객체가 어떤 특성을 가지고 있다&lt;/strong&gt;고 해서 &lt;strong&gt;반드시 그와 관련된 행위를 수행할 것이라고 가정&lt;/strong&gt;하지 않는다.&lt;ul&gt;
&lt;li&gt;즉, 객체를 고립된 상태로 놓고 어떤 책임이 적절한지를 결정하지 않는다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;law-of-demeter-묻지-말고-시켜라&quot;&gt;Law of Demeter (묻지 말고 시켜라)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;What/Who 사이클에 따라 객체에게 요청을 보낼 땐, &lt;strong&gt;어떤 객체가 수신하는지 보다 어떤 메시지를 보낼지에 더 집중&lt;/strong&gt;하게 된다&lt;ul&gt;
&lt;li&gt;따라서, 메시지를 보내는 입장에선 수신자가 어떤 객체인지 모르고, 객체의 내부 상태를 가정할 수도 없고, 이에 따라 당연히 &lt;strong&gt;객체의 결정에 간섭할 수 없다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;즉, &lt;strong&gt;어떻게&lt;/strong&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;ul&gt;
&lt;li&gt;메시지의 처리 방법은 수신 객체가 스스로 결정하게 하라&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;메시지를 중심으로 설계된 구조는 유연하고 확장 가능하며 재사용 가능하다. (p.161)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;객체-인터페이스&quot;&gt;객체 인터페이스&lt;/h2&gt;
&lt;h3 id=&quot;인터페이스&quot;&gt;인터페이스&lt;/h3&gt;
&lt;h4 id=&quot;인터페이스의-세-가지-특징&quot;&gt;인터페이스의 세 가지 특징&lt;/h4&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 id=&quot;메시지-1&quot;&gt;메시지&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;메시지는 객체간의 유일한 상호작용 수단이므로 객체가 수신할 수 있는 메시지의 목록이 객체가 제공하는 인터페이스의 모양을 결정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;공용-인터페이스&quot;&gt;공용 인터페이스&lt;/h3&gt;
&lt;ul&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;/ul&gt;
&lt;h2 id=&quot;인터페이스와-구현의-분리&quot;&gt;인터페이스와 구현의 분리&lt;/h2&gt;
&lt;h3 id=&quot;객체지향적인-사고-방식을-이해하기-위한-3가지-원칙&quot;&gt;객체지향적인 사고 방식을 이해하기 위한 3가지 원칙&lt;/h3&gt;
&lt;ol&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;strong&gt;메시지를 먼저 결정&lt;/strong&gt;하면 실제로 &lt;strong&gt;협력&lt;/strong&gt;에 필요한 메시지만 인터페이스로 노출할 수 있다&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;인터페이스와 &lt;strong&gt;구현&lt;/strong&gt; 간에 차이가 있다는 점을 인식&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;구현-3가지-원칙-중-마지막&quot;&gt;구현 (3가지 원칙 중 마지막)&lt;/h3&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;h3 id=&quot;인터페이스와-구현의-분리-원칙&quot;&gt;인터페이스와 구현의 분리 원칙&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체의 상태와 메서드 구현 등 '구현'과 공용 인터페이스를 명확하게 분리하라는 원칙&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;소프트웨어는 항상 변경&lt;/strong&gt;되고, 현재 객체가 다른 어떤 객체에 영향을 주고 있는지 전부 파악하는 것은 불가능하다&lt;/li&gt;
&lt;li&gt;따라서, 내부 구현이 변경되더라도 외부 객체에 영향을 주지 않도록 &lt;strong&gt;두 영역을 명확히 분리&lt;/strong&gt;하는 것이 필요하다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캡슐화&lt;/strong&gt;를 통해 달성 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;캡슐화&quot;&gt;캡슐화&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체의 자율성을 보존하기 위해 &lt;strong&gt;구현을 외부로부터 감추는 것&lt;/strong&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;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;/ul&gt;
&lt;h2 id=&quot;책임의-자율성이-협력의-품질을-결정한다&quot;&gt;책임의 자율성이 협력의 품질을 결정한다&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;객체의 책임이 자율적일수록 협력이 이해하기 쉬워지고, 유연하게 변경할 수 있게 된다&lt;/li&gt;
&lt;li&gt;다섯째, 객체가 수행하는 책임들이 자율적일수록 객체의 역할을 이해하기 쉬워진다&lt;ul&gt;
&lt;li&gt;객체는 동일한 목적을 달성하는 강하게 연관된 책임으로 구성되기 때문에 책임이 자율적일수록 객체의 &lt;strong&gt;응집도&lt;/strong&gt;를 높은 상태로 유지하기가 쉬워진다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;이 게시글은 스터디에서 [ &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001628109&quot;&gt;객체지향의 사실과 오해 / 조영호&lt;/a&gt; ] 책을 읽고 중요한 내용을 잊지 않기 위해 정리한 게시글입니다. 요약 및 생략된 내용이 많고 제가 이해한 대로 다시 정리한 내용이라 보다 자세하고 정확한 설명은 책 구매를 권장합니다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/92</guid>
      <comments>https://gmelon.dev/92#entry92comment</comments>
      <pubDate>Tue, 4 Apr 2023 16:06:32 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - 그래프] 다익스트라 - 가장 먼 노드</title>
      <link>https://gmelon.dev/91</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/49189&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제는 &lt;strong&gt;하나의 노드에서 다른 모든 노드까지의 최소 경로&lt;/strong&gt;를 구하는 문제로 &lt;strong&gt;다익스트라 알고리즘&lt;/strong&gt;을 이용해 풀이할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;다익스트라-알고리즘&quot;&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;code&gt;방문하지 않은 &amp;amp;&amp;amp; 최단 거리가 가장 짧은&lt;/code&gt; 노드를 선택&lt;/li&gt;
&lt;li&gt;3에서 선택한 노드를 거쳐 다른 노드로 가는 비용을 계산해 최단 거리 테이블을 갱신&lt;/li&gt;
&lt;li&gt;끝날 때 까지 3, 4번 과정을 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;문제의-테스트-케이스에-적용&quot;&gt;문제의 테스트 케이스에 적용&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;1번 노드부터 나머지 노드로의 최단 경로를 구하는 문제이므로 출발 노드는 &lt;strong&gt;1번&lt;/strong&gt;으로 설정&lt;/li&gt;
&lt;li&gt;간선에 가중치가 없으므로 출발노드와 이웃하는 노드까지의 거리를 1로 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTdqCu/btr6NZBFLiu/2jRAkO225OB6bVDSilIKrk/img.png&quot; alt=&quot;image-20230330123046609&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zAsv7/btr6Oqr5skU/5ZyyjabKE11IngP8yfja9k/img.png&quot; alt=&quot;image-20230330123630196&quot; /&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;방문하지 않은 노드 중 최단 경로가 최소인 2(또는 3)번 노드를 선택&lt;/li&gt;
&lt;li&gt;이웃한 노드의 최단 경로를 갱신&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kVSER/btr6YQW6BoJ/uzbLAE3lOmj5sns1Cb6PK0/img.png&quot; alt=&quot;image-20230330123719653&quot; /&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;방문하지 않은 노드 중 최단 경로가 최소인 3번 노드를 선택&lt;/li&gt;
&lt;li&gt;이웃한 노드의 최단 경로를 갱신&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0VFiV/btr6PBGLKnT/J6oFLXrMlJpS3vBi1f0onk/img.png&quot; alt=&quot;image-20230330123828505&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이 과정을 끝까지 반복하면 아래와 같은 상태가 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8J5Us/btr6N4v573y/fmveic559HUt0262J7NAMk/img.png&quot; alt=&quot;image-20230330123818438&quot; /&gt;&lt;/p&gt;
&lt;p&gt;따라서 최단 경로가 최대인 노드의 개수는 &lt;strong&gt;3개&lt;/strong&gt;이다.&lt;/p&gt;
&lt;h4 id=&quot;그리디-알고리즘&quot;&gt;그리디 알고리즘&lt;/h4&gt;
&lt;p&gt;다익스트라 알고리즘은 매 단계마다 최초 노드에서 경로가 최단인 노드를 선택하고 &lt;strong&gt;선택된 노드의 최단 경로를 그 시점에 확정&lt;/strong&gt;하기 때문에 매 시점에 가장 최적의 해를 선택한다는 점에서 그리디 알고리즘에 속한다고 할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;코드-설명-포함&quot;&gt;코드 (설명 포함)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;import java.util.*;

class Solution {

    // 우선순위 큐에서 사용하기 위한 Node 클래스
    static class Node implements Comparable&amp;lt;Node&amp;gt;{
        int index; // 현재 노드의 index
        int currentDistance; // 현재 노드의 현재까지의 최단 경로

        Node(int index, int currentDistance) {
            this.index = index;
            this.currentDistance = currentDistance;
        }

        // 우선순위 큐에서 대소 비교를 위해 Comparable 인터페이스를 구현해야 함
        @Override
        public int compareTo(Node other) {
            return this.currentDistance - currentDistance;
        }
    }

    public int solution(int n, int[][] edge) {
        int[] distances = new int[n + 1]; // 매 시점 노드까지의 최단거리를 저장
        Arrays.fill(distances, Integer.MAX_VALUE); // 최초엔 무한(도달 불가)으로 채우기

        List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; graph = new ArrayList&amp;lt;&amp;gt;(); // 각 노드와 이웃한 노드 리스트를 저장

        // graph 초기화
        for (int i = 0 ; i &amp;lt;= n ; i++) {
            graph.add(new ArrayList&amp;lt;&amp;gt;());
        }
        // 이웃 노드 정보 추가 (양방향)
        for (int[] e : edge) {
            graph.get(e[0]).add(e[1]);
            graph.get(e[1]).add(e[0]);
        }

        // 다익스트라 알고리즘 시작

        // 매 step에 가장 최단 경로가 짧은 노드를 선택하기 위해 반복문을 도는 대신 우선순위 큐를 사용
        PriorityQueue&amp;lt;Node&amp;gt; queue = new PriorityQueue&amp;lt;&amp;gt;();
        // 초기 (1번) 노드 설정
        queue.offer(new Node(1, 0)); // Node(index, currentDistance)
        distances[1] = 0;

        while(!queue.isEmpty()) {
            Node currentNode = queue.poll();
            // 이전에 방문한 노드면 제외
            // 우선순위 큐에서 빼온 currentDistance 값이 최단 경로 테이블에 기록된 최소 경로 값보다 크면
            // 이미 방문한 노드이므로 처리할 필요 없음
            if (currentNode.currentDistance &amp;gt; distances[currentNode.index]) {
                continue;
            }

            // 연결된 노드들 갱신
            for (int neighbor : graph.get(currentNode.index)) {
                int newDistance = distances[currentNode.index] + 1;
                // 새로운 경로가 더 짧을 경우에만 최단 경로 테이블 갱신 및 큐에 추가
                if (newDistance &amp;lt; distances[neighbor]) {
                    distances[neighbor] = newDistance;
                    queue.offer(new Node(neighbor, newDistance));
                }
            }       
        }

        // 최대 경로가 몇인지 탐색
        int maxDistance = 0;
        for (int i = 1 ; i &amp;lt;= n ; i++) {
            maxDistance = Math.max(maxDistance, distances[i]);
        }

        // 최대 경로를 갖는 노드가 몇 개인지 탐색
        int count = 0;
        for (int distance : distances) {
            if (distance == maxDistance) {
                count++;
            }
        }
        return count;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=acqm9mM1P6o&amp;list=PLRx0vPvlEmdAghTr5mXQxGpHjWqSz0dgC&amp;index=7&quot;&gt;https://www.youtube.com/watch?v=acqm9mM1P6o&amp;list=PLRx0vPvlEmdAghTr5mXQxGpHjWqSz0dgC&amp;index=7&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/91</guid>
      <comments>https://gmelon.dev/91#entry91comment</comments>
      <pubDate>Thu, 30 Mar 2023 12:48:43 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 4장 - 역할, 책임, 협력</title>
      <link>https://gmelon.dev/89</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;blockquote&gt;
  &lt;p&gt;객체지향에 갓 입문한 사람들의 가장 흔한 실수는 협력이라는 &lt;strong&gt;문맥을 고려하지 않은 채 객체가 가져야 할 행동부터 고민&lt;/strong&gt;하기 시작한다는 것이다. 중요한 것은 개별 객체(의 행동이나 상태)가 아니라 &lt;strong&gt;객체들 사이에 이뤄지는 협력&lt;/strong&gt;이다. 협력이 자리를 잡으면 저절로 객체의 행동이 드러나고 뒤이어 적절한 객체의 상태가 결정된다. (p.109)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;협력&quot;&gt;협력&lt;/h2&gt;
&lt;h3 id=&quot;요청과-응답&quot;&gt;요청과 응답&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;협력은 한 사람이 다른 사람에게 &lt;strong&gt;도움을 요청할 때 시작&lt;/strong&gt;됨&lt;/li&gt;
&lt;li&gt;요청을 받은 사람은 지식 또는 서비스를 제공함으로서 요청에 응답&lt;ul&gt;
&lt;li&gt;요청에 응답하는 과정에서 다시 다른 사람에게 요청을 보내야될 수도 있음&lt;/li&gt;
&lt;li&gt;즉, &lt;strong&gt;협력은 다수의 연쇄적인 요청 &amp; 응답의 흐름&lt;/strong&gt;으로 구성&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;어떤 사람이 특정한 요청을 받아들일 수 있는 이유는, 그 요청에 대해 &lt;strong&gt;적절한 방식으로 응답하는 데 필요한 지식과 행동 방식을 가지고 있기&lt;/strong&gt; 때문&lt;ul&gt;
&lt;li&gt;요청과 응답은 객체가 &lt;strong&gt;수행할 책임을 정의&lt;/strong&gt;한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;책임&quot;&gt;책임&lt;/h2&gt;
&lt;blockquote&gt;
  &lt;p&gt;어떤 객체가 &lt;strong&gt;어떤 요청에 대해 대답해 줄 수 있거나, 적절한 행동을 할 의무가 있는 경우&lt;/strong&gt; 해당 객체가 &lt;strong&gt;책임을 가진다&lt;/strong&gt;고 말한다. (중략) 결국 어떤 대상에 대한 요청은 그 대상이 요청을 처리할 책임이 있음을 암시한다. (p.114)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;책임은 객체지향 &lt;strong&gt;설계의 가장 중요한 재료&lt;/strong&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 id=&quot;책임의-분류&quot;&gt;책임의 분류&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;책임은 객체가 &lt;strong&gt;무엇을 알고 있는가&lt;/strong&gt;와 &lt;strong&gt;무엇을 할 수 있는가&lt;/strong&gt;로 구성됨&lt;/li&gt;
&lt;li&gt;일반적으로 책임을 이야기할 땐 &lt;strong&gt;외부에서 접근 가능한 공용 서비스의 관점&lt;/strong&gt;에서 이야기함&lt;ul&gt;
&lt;li&gt;즉, 책임은 객체의 &lt;strong&gt;외부에 제공해 줄 수 있는 정보(아는 것) + 외부에 제공해 줄 수 있는 서비스(하는 것)&lt;/strong&gt;의 목록&lt;/li&gt;
&lt;li&gt;이는 결과적으로 &lt;strong&gt;공용 인터페이스&lt;/strong&gt;를 구성&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;크레이그-라만의-객체-책임-분류&quot;&gt;크레이그 라만의 객체 책임 분류&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;하는 것 (doing)&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;아는 것 (knowing)&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;/ul&gt;
&lt;h3 id=&quot;책임과-메시지&quot;&gt;책임과 메시지&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;메시지 전송&lt;/strong&gt; - 객체가 다른 객체에게 &lt;strong&gt;주어진 책임을 수행하도록 요청을 보내는&lt;/strong&gt; 것&lt;ul&gt;
&lt;li&gt;즉, 두 객체 간의 협력은 &lt;strong&gt;오직 메시지&lt;/strong&gt;를 통해 이뤄짐&lt;/li&gt;
&lt;li&gt;두 객체가 서로 협력할 수 있는 것은 객체 A는 &lt;strong&gt;객체 B가 이해할 수 있는 메시지를 전송&lt;/strong&gt;할 수 있고, 객체 B는 &lt;strong&gt;객체 A가 전송하는 메시지에 대해 적절한 책임을 수행&lt;/strong&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;li&gt;&lt;strong&gt;책임과 협력(메시지)의 구조&lt;/strong&gt;가 완성되기 전엔 책임을 어떻게 구현할지에 대한 고민은 미뤄두는 것이 좋다&lt;ul&gt;
&lt;li&gt;어떤 객체가 1. &lt;strong&gt;어떤 책임을 수행&lt;/strong&gt;해야 하고 2. &lt;strong&gt;어떤 객체로부터 어떤 메시지를 수신&lt;/strong&gt;할 것인지를 결정하는 것으로 부터 객체지향 설계가 시작됨&lt;/li&gt;
&lt;li&gt;클래스 및 메서드의 도출은 그 이후에 해도 늦지 않음&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;역할&quot;&gt;역할&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;어떤 객체가 수행하는 &lt;strong&gt;책임의 집합&lt;/strong&gt;은 객체가 &lt;strong&gt;협력 안에서 수행하는 역할&lt;/strong&gt;을 암시&lt;/li&gt;
&lt;li&gt;역할은 재사용 가능하고 유연한 객체지향 설계를 낳는 매우 중요한 구성요소&lt;ul&gt;
&lt;li&gt;역할은 협력 내에서 &lt;strong&gt;다른 객체로 대체할 수 있음&lt;/strong&gt;을 나타내는 일종의 &lt;strong&gt;표식&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;역할은 &lt;code&gt;&quot;이 자리는 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있습니다&quot;&lt;/code&gt;고 말하는 것과 같음&lt;/li&gt;
&lt;li&gt;역할을 대체할 수 있는 객체는 &lt;strong&gt;동일한 메시지를 이해할 수 있는&lt;/strong&gt; 객체로 한정됨&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;동일한 역할을 수행하는 객체들이 동일한 메시지를 수신할 수 있기 때문에 동일한 책임을 수행할 수 있다는 것은 매우 중요한 개념이다. 이 개념을 제대로 이해해야만 객체지향이 제공하는 많은 장점을 누릴 수 있다. (p.126)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;협력의-추상화&quot;&gt;협력의 추상화&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;역할을 통해 하나의 협력 안에 &lt;strong&gt;여러 종류의 객체가 참여&lt;/strong&gt;하게 할 수 있음 -&amp;gt; &lt;strong&gt;협력을 추상화&lt;/strong&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 id=&quot;대체-가능성&quot;&gt;대체 가능성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;역할은 본질적으로 &lt;strong&gt;다른 객체에 의해 대체 가능&lt;/strong&gt;함을 의미&lt;ul&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;ul&gt;
&lt;li&gt;이는 일반화 / 특수화 관점과도 연관 (역할 - 일반화, 특정 객체 - 특수화)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;객체의-모양을-결정하는-협력&quot;&gt;객체의 모양을 결정하는 협력&lt;/h2&gt;
&lt;blockquote&gt;
  &lt;p&gt;데이터는 단지 객체가 행위를 수행하는 데 필요한 재료일 뿐이다. 객체가 존재하는 이유는 &lt;strong&gt;행위(책임)를 수행하며 협력에 참여&lt;/strong&gt;하기 위해서다.(p.128)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;애플리케이션을 설계할 땐 각 객체를 독립적으로 바라보지 말고, &lt;strong&gt;협력이라는 문맥&lt;/strong&gt;을 고려해야 함&lt;ul&gt;
&lt;li&gt;실제로 동작하는 애플리케이션을 구축하기 위해선 &lt;strong&gt;해당 객체가 참여하는 협력&lt;/strong&gt;을 우선적으로 고려해야 함&lt;/li&gt;
&lt;li&gt;협력을 배제한 상태에서 &lt;strong&gt;어떤 데이터가 필요하고 어떤 클래스로 구현해야 하는지 고민하는 것&lt;/strong&gt;은 아무런 도움이 되지 않는다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;협력을-따라-흐르는-객체의-책임&quot;&gt;협력을 따라 흐르는 객체의 책임&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;먼저, 견고하고 깔끔한 &lt;strong&gt;협력을 설계&lt;/strong&gt;할 것&lt;ul&gt;
&lt;li&gt;협력 설계 -&amp;gt; &lt;strong&gt;객체들이 주고받을 요청과 응답의 흐름을 결정&lt;/strong&gt;하는 것을 의미&lt;/li&gt;
&lt;li&gt;이 흐름은 &lt;strong&gt;객체들이 수행할 책임&lt;/strong&gt;이 된다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;⭐️-아래-순서로-객체-설계를-진행하기-p129&quot;&gt;⭐️ 아래 순서로 객체 설계를 진행하기 (p.129)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;협력 설계 -&amp;gt; 책임(외부에 제공할 행동) 할당 -&amp;gt; 필요한 데이터 결정 -&amp;gt; 클래스 구현 방법 결정&lt;/li&gt;
&lt;li&gt;책임 결정 및 할당 과정이 얼마나 합리적이고 적절하게 수행되었는지가 설계의 품질을 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체지향 시스템에서 가장 중요한 것은 &lt;strong&gt;충분히 자율적인 동시에 충분히 협력적인 객체&lt;/strong&gt;를 창조하는 것이다. 이 목표를 달성할 수 있는 가장 쉬운 방법은 객체를 충분히 협력적으로 만든 후에 &lt;strong&gt;협력이라는 문맥 안에서 객체를 충분히 자율적으로 만드는 것&lt;/strong&gt;이다. (p.130)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;객체지향-설계-기법&quot;&gt;객체지향 설계 기법&lt;/h2&gt;
&lt;h3 id=&quot;책임-주도-설계&quot;&gt;책임 주도 설계&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;시스템이 사용자에게 제공해야 하는 기능인 &lt;strong&gt;시스템 책임을 파악&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;시스템 책임을 &lt;strong&gt;더 작은 책임&lt;/strong&gt;으로 분할&lt;ul&gt;
&lt;li&gt;분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 &lt;strong&gt;책임을 할당&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;객체의 책임 수행 도중에 &lt;strong&gt;다른 객체의 도움이 필요&lt;/strong&gt;한 경우, 이를 책임질 적절한 객체 또는 역할을 찾음&lt;ul&gt;
&lt;li&gt;해당 객체 또는 역할에게 책임을 할당함으로써 &lt;strong&gt;두 객체가 협력&lt;/strong&gt;하게 됨&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;디자인-패턴&quot;&gt;디자인 패턴&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;디자인 패턴은 &lt;strong&gt;책임 주도 설계의 결과를 표현&lt;/strong&gt;한다&lt;ul&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;strong&gt;반복적으로 발생하는 문제&lt;/strong&gt;와 &lt;strong&gt;그 문제에 대한 해법&lt;/strong&gt;의 쌍으로 정의됨&lt;ul&gt;
&lt;li&gt;반복해서 일어나는 특정한 상황에서 어떤 설계가 왜 효과적인지에 대한 이유를 설명&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;중요한 것은, 디자인 패턴에서 말하는 구성 요소가 &lt;strong&gt;클래스와 메서드가 아니라 '협력'에 참여하는 '역할'과 '책임'이라는 것&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;즉, 실제 구현 시에는 다양한 방식으로 역할을 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;ex) &lt;strong&gt;여러 역할을 하나의 객체가 수행하게 하는 등&lt;/strong&gt;으로도 구현 가능&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;테스트-주도-개발&quot;&gt;테스트 주도 개발&lt;/h3&gt;
&lt;h4 id=&quot;기본-흐름&quot;&gt;기본 흐름&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;실패하는 테스트 작성 -&amp;gt; 테스트를 통과하는 가장 간단한 코드 작성 -&amp;gt; 리팩토링을 통해 코드의 중복 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;본질&quot;&gt;본질&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;테스트 주도 개발을 테스트를 작성하는 것이 아님&lt;ul&gt;
&lt;li&gt;책임을 수행할 객체 또는 클라이언트가 기대하는 객체의 역할이 &lt;strong&gt;메시지를 수신할 때 어떤 결과를 반환&lt;/strong&gt;하고 &lt;strong&gt;어떤 객체와 협력&lt;/strong&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;TDD는 테스트라는 안전장치를 통해 빠르고 견고하게 책임 주도 설계와 동일한 목적을 달성하게 해주는 것&lt;ul&gt;
&lt;li&gt;따라서, 다양한 설계 경험 및 패턴에 지식이 없는 사람들의 경우 온전한 혜택을 누리기 어렵다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;테스트를 작성하기 위해 객체의 메서드를 호출하고 반환 값을 검증하는 것은 &lt;strong&gt;순간적으로 객체가 수행해야 하는 책임에 대해 생각한 것&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;stub, mock 객체를 추가하는 것은 협력에 대한 고민&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;질문&quot;&gt;질문&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;이 게시글은 스터디에서 [ &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001628109&quot;&gt;객체지향의 사실과 오해 / 조영호&lt;/a&gt; ] 책을 읽고 중요한 내용을 잊지 않기 위해 정리한 게시글입니다. 요약 및 생략된 내용이 많고 제가 이해한 대로 다시 정리한 내용이라 보다 자세하고 정확한 설명은 책 구매를 권장합니다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/89</guid>
      <comments>https://gmelon.dev/89#entry89comment</comments>
      <pubDate>Sun, 26 Mar 2023 16:34:38 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - graph] 네트워크</title>
      <link>https://gmelon.dev/88</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;문제-이해&quot;&gt;문제 이해&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43162&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;문제에 &lt;code&gt;int[][]computers&lt;/code&gt; 배열로 컴퓨터의 연결에 대한 정보가 주어진다. 서로 연결된 컴퓨터들은 하나의 네트워크를 이룬다고 할 때 총 네트워크의 개수를 구하는 문제이다.&lt;/p&gt;
&lt;p&gt;예를 들어 연결이 아래와 같다면 네트워크는 2개이고&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZOIQu/btr5zRbXWUI/msshwWbKChFkcoksRPOjfK/img.png&quot; alt=&quot;image-20230323122038626&quot; /&gt;&lt;/p&gt;
&lt;p&gt;아래와 같다면 네트워크는 1개가 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDEwhD/btr5iadUL31/X0Iiy3qKE7V3eWSIaaFyg1/img.png&quot; alt=&quot;image-20230323122059651&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;p&gt;이 문제는 전형적인 그래프 탐색 문제로,  &lt;code&gt;visited&lt;/code&gt; 배열에  방문한 노드들을 저장하면서 노드를 탐색해나가는데, 새로운 네트워크가 발견될 때마다 해당 네트워크에 연결된 모든 노드를 &lt;code&gt;탐색&lt;/code&gt; 상태로 만들어둔다. 이렇게 하면 아직 탐색되지 않은 노드가 발견된다는 의미가 새로운 네트워크를 발견한다는 의미와 같아지므로 그때마다 count를 증가시켜서 총 네트워크의 개수를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public int solution(int n, int[][] computers) {
    boolean[] visited = new boolean[n];
    int networkCount = 0;

    for (int i = 0 ; i &amp;lt; n ; i++) {
        if (visited[i]) {
            continue;
        }
        networkCount++;
        visitAllComputersInNetwork(computers, visited, i);
    }

    return networkCount;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;먼저 위 코드를 통해 visited 배열을 만들고, 전체 노드 (0 ~ n) 을 순회하며 현재 노드가 &lt;strong&gt;아직 방문되지 않았을 경우에만&lt;/strong&gt; 해당 노드가 속한 네트워크의 모든 노드를 &lt;code&gt;visitAllComputersInNetwork()&lt;/code&gt; 메서드를 통해 탐색한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public void visitAllComputersInNetwork(final int[][] computers, boolean[] visited, int startIndex) {
    Stack&amp;lt;Integer&amp;gt; stack = new Stack&amp;lt;&amp;gt;();
    stack.push(startIndex);

    while(!stack.isEmpty()) {
        int currentIndex = stack.pop();
        visited[currentIndex] = true;

        // 인접 노드 stack에 추가
        for (int i = 0 ; i &amp;lt; computers[currentIndex].length ; i++) {
            if (!visited[i] &amp;amp;&amp;amp; !stack.contains(i) &amp;amp;&amp;amp; computers[currentIndex][i] == 1) {
                stack.push(i);   
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;visitAllComputersInNetwork()&lt;/code&gt; 메서드는 Stack을 이용해서 DFS로 노드들을 탐색한다. 탐색하면서 현재 노드는 탐색 완료 상태로 만들고, 새롭게 탐색한 노드(컴퓨터)에 인접한(연결된) 노드가 있다면 Stack에 추가해서 한번 탐색이 될 때 마다 해당 네트워크의 모든 노드가 탐색될 수 있도록 한다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/88</guid>
      <comments>https://gmelon.dev/88#entry88comment</comments>
      <pubDate>Thu, 23 Mar 2023 12:31:27 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] graph와 탐색</title>
      <link>https://gmelon.dev/87</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;비선형-자료구조&quot;&gt;비선형 자료구조&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;선형으로 표현할 수 없는 데이터&lt;/strong&gt;를 표현할 때 사용되는 자료구조&lt;ul&gt;
&lt;li&gt;예를 들어 지하철 노선도와 같이 하나의 선으로 표현할 수 없는 데이터를 표현하고자 할 때 사용된다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;대표적으로 graph가 비선형 구조이다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;graph&quot;&gt;graph&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;데이터를 갖는 &lt;strong&gt;Node&lt;/strong&gt;와 Node들을 이어주는 &lt;strong&gt;Edge&lt;/strong&gt;로 구성&lt;ul&gt;
&lt;li&gt;Edge는 단방향 / 양방향의 &lt;strong&gt;방향성&lt;/strong&gt;을 가질 수 있다&lt;/li&gt;
&lt;li&gt;Edge는 &lt;strong&gt;가중치&lt;/strong&gt;를 가질 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;데이터의-탐색&quot;&gt;데이터의 탐색&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;선형 자료구조와 달리 비선형 구조는 시작과 끝이 모호하다&lt;/li&gt;
&lt;li&gt;따라서 먼저 하나의 Node를 탐색하고&lt;ul&gt;
&lt;li&gt;해당 Node와 Edge로 연결된 Node들을 탐색한다&lt;ul&gt;
&lt;li&gt;연결된 Node들은 Queue / Stack 등에 넣어 탐색을 &lt;strong&gt;예약&lt;/strong&gt;해둔다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;원하는 Node를 찾을 때 까지 이 과정을 반복한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;탐색에-사용하는-자료구조에-따른-탐색-순서-차이&quot;&gt;탐색에 사용하는 자료구조에 따른 탐색 순서 차이&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;탐색에 Queue를 사용하면&lt;ul&gt;
&lt;li&gt;먼저 들어간 Node가 먼저 탐색되므로, 계속해서 가까운 Node로 &lt;strong&gt;넓게 퍼지는 형태로 Node들을 탐색&lt;/strong&gt;한다&lt;/li&gt;
&lt;li&gt;즉, 너비 우선 탐색 (BFS, Breadth-First Search) 가 된다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;탐색에 Stack을 사용하면&lt;ul&gt;
&lt;li&gt;더 이상 연결된 Node가 없을 때 까지 Stack 최상단 Node를 먼저 탐색하므로 &lt;strong&gt;가장 멀리 있는 Node를 향해 탐색&lt;/strong&gt;하는 형태를 띈다&lt;/li&gt;
&lt;li&gt;즉, 깊이 우선 탐색 (DFS, Depth-First Search) 가 된다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;코드&quot;&gt;코드&lt;/h2&gt;
&lt;h3 id=&quot;graph-생성&quot;&gt;graph 생성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;그래프를 표현할 수 있는 자료구조가 자바에 없으므로 직접 구현해 사용해야 한다. 자바에서 그래프를 구현할 수 있는 방법은 크게 1. 인접 리스트 와 2. 인접 행렬 이 있다&lt;ul&gt;
&lt;li&gt;인접 리스트는 각 노드가 자신과 &lt;strong&gt;이웃한 노드들의 리스트&lt;/strong&gt;를 가지고 있는 방식을 말한다&lt;ul&gt;
&lt;li&gt;인접 리스트 방식에서 가중치를 표현하기 위해서는 노드에 가중치 필드를 더해주거나 가중치를 위한 별도 리스트를 만들어야 한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;인접 행렬은 2차원 배열을 사용해 각 노드들이 연결되어 있는지를 표현한다. (ex. arr[3][1] == 1 이면 3번 노드에서 1번 노드로의 간선이 존재하는 식)&lt;ul&gt;
&lt;li&gt;간선에 가중치가 있는 경우 인접 행렬에 값을 넣어주는 방식으로 가중치를 표현할 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;인접-리스트-예제&quot;&gt;인접 리스트 예제&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class Node {
    Integer data;
    List&amp;lt;Node&amp;gt; edges = new LinkedList&amp;lt;&amp;gt;();
    boolean visited;

    public Node(int data) {
        this.data = data;
    }

    // link() 메서드를 통해 현재 노드에 이웃한 Node를 추가
    public void link(Node node) {
        this.edges.add(node);
    }

    // 양방향 연관관계 추가
    public void linkBiderectional(Node node) {
        this.link(node);
        node.link(this);
    }

    // 방문 여부 체크
    public void visit() {
        this.visited = true;
    }

    @Override
    public String toString() {
        return data.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;인접-행렬-예제&quot;&gt;인접 행렬 예제&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;정점이 1~N인 int형이라고 가정하면 아래와 같이 인접 행렬을 이용해 그래프를 표현할 수 있다&lt;ul&gt;
&lt;li&gt;정점이 int형이 아니라면, 정점들을 모아둔 &lt;code&gt;String[] nodes&lt;/code&gt; 와 같은 배열을 추가로 사용해 인접 여부와 정점의 값을 별도로 저장하는 방식을 사용할 수도 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public class Graph {
    int[][] matrix;
    boolean[] visited;

    public Graph(int size) {
        this.matrix = new int[size + 1][size + 1];
        this.visited = new boolean[size + 1];
    }

    public void link(int vertexA, int vertexB) {
        matrix[vertexA][vertexB] = 1;
    }

    public void linkBidirectional(int vertexA, int vertexB) {
        link(vertexA, vertexB);
        link(vertexB, vertexA);
    }

    public void visit(int vertex) {
        visited[vertex] = true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;bfs&quot;&gt;BFS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;맹목적인 탐색&lt;/strong&gt;을 하고자 할 때 사용&lt;/li&gt;
&lt;li&gt;최단 경로를 찾아주기 때문에, 경로가 최단거리임을 보장해야 할 때 사용 가능&lt;ul&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;img src=&quot;https://blog.kakaocdn.net/dn/HCIZA/btr5dd8lY75/TBF8YHus5UWlkenNgkbLl1/img.png&quot; alt=&quot;image-20230321163253407&quot; /&gt;&lt;/p&gt;
&lt;p&gt;그래프는 아래와 같이 구성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// graph 구성
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
Node node6 = new Node(6);
Node node7 = new Node(7);

node1.linkBiderectional(node2);
node1.linkBiderectional(node3);

node2.linkBiderectional(node4);
node2.linkBiderectional(node5);

node3.linkBiderectional(node6);
node3.linkBiderectional(node7);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BFS 코드는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static void bfs(Node head, Integer target) {
    Queue&amp;lt;Node&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
    queue.offer(head);

    while (!queue.isEmpty()) {
        Node currentNode = queue.poll();
        // 현재 노드 방문 처리
        currentNode.visit();
        System.out.println(currentNode);

        // 타겟 여부 확인
        if (currentNode.data == target) {
            System.out.println(&quot;Target 발견 : &quot; + currentNode.data);
            break;
        }

        // 이웃 노드 Queue에 삽입
        for (Node node : currentNode.edges) {
            if (!node.visited &amp;amp;&amp;amp; !queue.contains(node)) {
                queue.offer(node);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행해보면, 아래와 같이 1 -&amp;gt; 2 -&amp;gt; 3 -&amp;gt; … 순으로 (너비 우선) 탐색되는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 실행 코드
bfs(node1, 7);

// 출력
1
2
3
4
5
6
7
Target 발견 : 7&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;dfs&quot;&gt;DFS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;DFS는 기본적으로 Stack을 사용해서 구현하지만, 컴퓨터가 메서드를 호출할 때 스택 프레임이 생성되므로 DFS는 &lt;strong&gt;재귀&lt;/strong&gt;를 이용해도 구현할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;stack-사용&quot;&gt;Stack 사용&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Stack을 사용해 DFS를 구현하는 경우, BFS 코드에서 Queue만 Stack으로 바뀌었을 뿐 나머지는 전혀 바뀌지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static void dfs(Node head, Integer target) {
    Stack&amp;lt;Node&amp;gt; stack = new Stack&amp;lt;&amp;gt;();
    stack.push(head);

    while (!stack.isEmpty()) {
        Node currentNode = stack.pop();
        // 현재 노드 방문 처리
        currentNode.visit();
        System.out.println(currentNode);

        // 타겟 여부 확인
        if (currentNode.data == target) {
            System.out.println(&quot;Target 발견 : &quot; + currentNode.data);
            break;
        }

        // 이웃 노드 Stack에 삽입
        for (Node node : currentNode.edges) {
            if (!node.visited &amp;amp;&amp;amp; !stack.contains(node)) {
                stack.push(node);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;재귀-사용&quot;&gt;재귀 사용&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;재귀를 사용할 경우 아래와 같이 메서드 호출이 stack push / pop 연산과 동일하게 동작한다.&lt;ul&gt;
&lt;li&gt;메서드&lt;strong&gt;를 호출&lt;/strong&gt; == Stack에 node를 push() 하는 것&lt;/li&gt;
&lt;li&gt;메서드&lt;strong&gt;가 호출됨&lt;/strong&gt; == Stack에서 node를 pop() 하는 것&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;public static void dfsRecursion(Node head, Integer target) {
    // 현재 노드 방문 처리
    head.visit();
    System.out.println(head);

    // 타겟 여부 확인
    if (head.data == target) {
        System.out.println(&quot;Target 발견 : &quot; + head.data);
    }

    // 이웃 노드 호출
    for (Node node : head.edges) {
        if (!node.visited) {
            dfsRecursion(node, target);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실행해보면 BFS와 달리 깊이 우선으로 탐색되는 것을 알 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;// 실행 코드
dfsStack(node1, 7);

// 출력
1
3
7
Target 발견 : 7

// 실행 코드
dfsRecursion(node1, 7);

// 출력
1
2
4
5
3
6
7
Target 발견 : 7&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;레퍼런스&quot;&gt;레퍼런스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/13577&quot;&gt;https://school.programmers.co.kr/learn/courses/13577&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=66ZKz-FktXo&amp;list=PLRx0vPvlEmdDHxCvAQS1_6XV4deOwfVrz&amp;index=16&amp;ab_channel=%EB%8F%99%EB%B9%88%EB%82%98&quot;&gt;https://www.youtube.com/watch?v=66ZKz-FktXo&amp;list=PLRx0vPvlEmdDHxCvAQS1_6XV4deOwfVrz&amp;index=16&amp;ab_channel=%EB%8F%99%EB%B9%88%EB%82%98&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=l0Rsu7dziws&amp;list=PLRx0vPvlEmdDHxCvAQS1_6XV4deOwfVrz&amp;index=17&amp;ab_channel=%EB%8F%99%EB%B9%88%EB%82%98&quot;&gt;https://www.youtube.com/watch?v=l0Rsu7dziws&amp;list=PLRx0vPvlEmdDHxCvAQS1_6XV4deOwfVrz&amp;index=17&amp;ab_channel=%EB%8F%99%EB%B9%88%EB%82%98&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/87</guid>
      <comments>https://gmelon.dev/87#entry87comment</comments>
      <pubDate>Tue, 21 Mar 2023 17:07:04 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 3장 - 타입과 추상화</title>
      <link>https://gmelon.dev/86</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;blockquote&gt;
  &lt;p&gt;초기의 지하철 노선도는 실제와 유사한 물리적인 지형 위에 구불구불한 운행 노선과 불규칙적인 역 간의 거리를 사실적으로 묘사하고 있었다. 문제는 이렇게 &lt;strong&gt;사실적인 정보가 오히려&lt;/strong&gt; 지하철을 이용하는 승객들로 하여금 노선도를 &lt;strong&gt;이해하기 어렵게 만들었다는 점&lt;/strong&gt;이다. (p.73)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
  &lt;p&gt;해리 벡은 승객이 꼭 알아야 하는 사실만을 정확하게 표현하고 몰라도 되는 정보는 무시함으로써 이해하기 쉽고 단순하며 목적에 부합하는 지하철 노선도를 창조해 낼 수 있었다. (p.75)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;추상화를-통한-복잡성-극복&quot;&gt;추상화를 통한 복잡성 극복&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;진정한 의미에서 추상화란 현실에서 출발하되 불필요한 부분을 도려내가면서 &lt;strong&gt;사물의 놀라운 본질을 드러나게 하는 과정&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;불필요한 부분을 무시해 &lt;strong&gt;현실에 존재하는 복잡성을 극복&lt;/strong&gt;하는게 추상화의 목적&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;훌륭한 추상화는 &lt;strong&gt;목적에 부합&lt;/strong&gt;하는 것이어야 한다&lt;ul&gt;
&lt;li&gt;추상화의 수준, 이익, 가치는 목적에 의존적이다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;모든 경우에 추상화의 목적은 &lt;strong&gt;복잡성을 이해하기 쉬운 수준으로 단순화&lt;/strong&gt;하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;추상화의-두-가지-차원&quot;&gt;추상화의 두 가지 차원&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;구체적 사물 간 공통점은 취하고 차이점을 버리는 일반화&lt;/li&gt;
&lt;li&gt;중요한 부분을 강조하기 위해 불필요한 세부 사항을 제거&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;객체지향과-추상화&quot;&gt;객체지향과 추상화&lt;/h2&gt;
&lt;h3 id=&quot;개념&quot;&gt;개념&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;공통점을 기반으로 객체들을 묶기 위한 그릇&lt;ul&gt;
&lt;li&gt;일반적으로 우리가 인식하고 있는 다양한 사물이나 객체에 적용할 수 있는 아이디어나 관념을 뜻함&lt;/li&gt;
&lt;li&gt;전혀 다른 별개의 사물을 &lt;strong&gt;공통점을 기반으로&lt;/strong&gt; 하나의 개념으로 묶음으로써 여러 사물을 개별적으로 다뤄야 하는 복잡한 상황을 피할 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;개념을 사용하면 결과적으로 사물을 여러 그룹으로 &lt;strong&gt;분류&lt;/strong&gt;할 수 있게 된다&lt;ul&gt;
&lt;li&gt;특정 그룹(개념)에 속하는 객체를 해당 개념의 &lt;strong&gt;인스턴스(instance)&lt;/strong&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;/ul&gt;
&lt;h3 id=&quot;개념의-세-가지-관점&quot;&gt;개념의 세 가지 관점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;심볼(symbol) - 개념의 명칭&lt;/li&gt;
&lt;li&gt;내연(intension) - 개념의 완전한 정의, 내연의 의를 통해 객체가 개념에 속하는지를 판단&lt;/li&gt;
&lt;li&gt;외연(extension) - 개념에 속하는 모든 객체들의 집합&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;객체를-분류하기-위한-틀&quot;&gt;객체를 분류하기 위한 틀&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;분류 -&amp;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;/ul&gt;
&lt;h2 id=&quot;타입&quot;&gt;타입&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;타입&lt;/strong&gt;의 정의는 &lt;strong&gt;개념&lt;/strong&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 id=&quot;데이터-타입&quot;&gt;데이터 타입&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;타입 시스템의 목적은 메모리 안의 모든 데이터가 비트열로 보임으로써 야기되는 &lt;strong&gt;혼란을 방지&lt;/strong&gt;하는 것&lt;ul&gt;
&lt;li&gt;타입 시스템은 0과 1 (bit string)에 대해 &lt;strong&gt;수행가능한 작업과 불가능한 작업을 구분&lt;/strong&gt;함으로써 데이터가 잘못 사용되는 것을 방지한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;타입에-관련된-두-가지-중요한-사실&quot;&gt;타입에 관련된 두 가지 중요한 사실&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;어떤 데이터에 어떤 연산자를 적용할 수 있느냐&lt;/strong&gt;가 그 데이터의 타입을 결정한다&lt;/li&gt;
&lt;li&gt;타입에 속한 데이터를 메모리에 &lt;strong&gt;어떻게 표현하는지는 외부로부터 철저하게 감춰진다&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;객체와-타입&quot;&gt;객체와 타입&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체를 타입에 따라 분류하고, 그 타입에 이름을 붙이는 것은 결국 새로운 데이터 타입을 선언하는 것과 같다&lt;ul&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;/ul&gt;
&lt;h4 id=&quot;타입에-관련된-두-가지-사실은-객체에도-그대로-적용된다&quot;&gt;타입에 관련된 두 가지 사실은 객체에도 그대로 적용된다&lt;/h4&gt;
&lt;ul&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;/ul&gt;
&lt;h2 id=&quot;타입의-계층&quot;&gt;타입의 계층&lt;/h2&gt;
&lt;h3 id=&quot;일반화--특수화-관계&quot;&gt;일반화 / 특수화 관계&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;타입과 타입사이에는 일반화 / 특수화 관계가 존재할 수 있다&lt;ul&gt;
&lt;li&gt;일반화 -&amp;gt; 포괄적이라는 의미로, 더 넓은 범위를 나타낸다&lt;/li&gt;
&lt;li&gt;특수화 -&amp;gt; 더 좁은 범위의 객체를 포함, 집합의 관점에서 보면 일반화 개념의 부분집합&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;일반화 / 특수화 관계를 나타내는 것은 &lt;strong&gt;상태를 표현하는 데이터가 아니라 행동&lt;/strong&gt; 이다.&lt;ul&gt;
&lt;li&gt;한 타입이 다른 타입보다 더 특수하게 / 일반적으로 행동해야 이 관계가 성립한다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;더 일반적인 / 특수한 데이터(상태)를 가진다고 관계가 성립하는 것이 아니다!&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;특수한--일반적인-행동이란&quot;&gt;특수한 / 일반적인 행동이란?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;특수한 행동을 한다는 것은 일반화 타입이 할 수 있는 모든 행동을 동일하게 수행하면서 추가적인 행동을 갖는 것을 의미한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;슈퍼타입--서브타입&quot;&gt;슈퍼타입 / 서브타입&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;일반화 / 특수화 관계에서 &lt;ul&gt;
&lt;li&gt;좀 더 일반적인 타입을 슈퍼타입(supertype)&lt;/li&gt;
&lt;li&gt;좀 더 특수한 타입을 서브타입(subtype) 이라고 한다&lt;/li&gt;&lt;/ul&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;h3 id=&quot;일반화는-추상화를-위한-도구&quot;&gt;일반화는 추상화를 위한 도구&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;서브타입에서만 제공하는 특수한 행동이 필요없을 때 서브타입을 슈퍼타입으로 일반화할 수 있다&lt;ul&gt;
&lt;li&gt;ex) 트럼프 인간을 단순히 &lt;strong&gt;트럼프&lt;/strong&gt;라고 일반화해서 인식&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;이때, 1. 공통점을 그룹화하고 &lt;strong&gt;2. 불필요한 특성을 제거&lt;/strong&gt;하는 &lt;strong&gt;두 가지 추상화 기법이 동시에 적용&lt;/strong&gt;된다&lt;ul&gt;
&lt;li&gt;따라서 일반화를 사용해 추상화를 이루고 있다고 할 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;정적-모델&quot;&gt;정적 모델&lt;/h2&gt;
&lt;h3 id=&quot;타입의-목적&quot;&gt;타입의 목적&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체지향은 객체를 지향하지만 인간의 인지 능력으로는 &lt;strong&gt;동적으로 변화는 객체의 복잡성&lt;/strong&gt;을 극복하기가 너무 어렵다&lt;ul&gt;
&lt;li&gt;때문에 &lt;strong&gt;타입을 사용&lt;/strong&gt;한다!&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;타입은 시간에 따라 &lt;strong&gt;동적으로 변하는 상태를 정적인 모습으로 다룰 수 있게&lt;/strong&gt; 해준다&lt;ul&gt;
&lt;li&gt;상태의 모든 경우의 수를 나열하는 대신, 해당 상태가 &lt;strong&gt;변할 수 있다&lt;/strong&gt; 고만 인식함으로써 상황을 단순하게 만들 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;타입--추상화&quot;&gt;타입 == 추상화&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;타입은 시간이라는 요소와 상태 변화라는 요소를 제거하고 정적인 관점에서 객체를 바라 볼 수 있게 해준다&lt;ul&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;/ul&gt;
&lt;h3 id=&quot;동적-모델-정적-모델&quot;&gt;동적 모델, 정적 모델&lt;/h3&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;h4 id=&quot;동적-모델-스냅샷&quot;&gt;동적 모델 (스냅샷)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;객체가 특정 시점에 구체적으로 어떤 상태를 가지느냐&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;정적-모델-타입-모델&quot;&gt;정적 모델 (타입 모델)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;객체가 가질 수 있는 모든 상태와 행동을 시간에 독립적으로 표현&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;클래스&quot;&gt;클래스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체지향 프로그래밍 언어에서 정적 모델은 &lt;strong&gt;클래스&lt;/strong&gt;를 이용해 구현됨&lt;/li&gt;
&lt;li&gt;클래스를 통해 &lt;strong&gt;타입을 구현&lt;/strong&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;li&gt;클래스와 타입을 구분하는 것은 &lt;strong&gt;설계를 유연하게 유지하기 위한 바탕&lt;/strong&gt;이 된다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;이 게시글은 스터디에서 [ &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001628109&quot;&gt;객체지향의 사실과 오해 / 조영호&lt;/a&gt; ] 책을 읽고 중요한 내용을 잊지 않기 위해 정리한 게시글입니다. 요약 및 생략된 내용이 많고 제가 이해한 대로 다시 정리한 내용이라 보다 자세하고 정확한 설명은 책 구매를 권장합니다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/86</guid>
      <comments>https://gmelon.dev/86#entry86comment</comments>
      <pubDate>Mon, 20 Mar 2023 17:37:11 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 2장 - 이상한 나라의 객체</title>
      <link>https://gmelon.dev/85</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;blockquote&gt;
  &lt;p&gt;세상을 더 작은 객체로 분해하는 것은 본질적으로 세상이 포함하고 있는 복잡성을 극복하기 위한 인간의 작은 몸부림이다.  인간은 좀 더 단순한 객체들로 주변을 분해함으로써 자신이 몸담고 있는 세상을 이해하려고 노력한다. (p.41)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체지향 패러다임의 목적은 &lt;strong&gt;현실 세계를 모방하는 것이 아니라 현실 세계를 기반으로 새로운 세계를 창조하는 것&lt;/strong&gt;이다. (중략) 현실 세계에서는 사람이 직접 주문 금액을 계산하지만 소프트웨어 세계에서는 주문 객체가 자신의 금액을 계산한다. (p.42)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;객체의-정의&quot;&gt;객체의 정의&lt;/h2&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체의 다양한 특성을 효과적으로 설명하기 위해서는 &lt;strong&gt;객체를 상태, 행동, 식별자를 지닌 실체&lt;/strong&gt;로 보는 것이 가장 효과적이다. (p.47)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;객체란 &lt;strong&gt;식별 가능한 개체 또는 사물&lt;/strong&gt;이다&lt;/li&gt;
&lt;li&gt;객체는 자동차처럼 만질 수 있는 &lt;strong&gt;구체적 사물&lt;/strong&gt;일 수도 있고, 시간처럼 &lt;strong&gt;추상적 개념&lt;/strong&gt;일 수도 있다&lt;/li&gt;
&lt;li&gt;객체는 &lt;code&gt;1. 구별 가능한 식별자 2. 특징적인 행동 3. 변경 가능한 상태&lt;/code&gt; 를 가진다&lt;/li&gt;
&lt;li&gt;소프트웨어 안에서 객체는 &lt;strong&gt;저장된 상태와 실행 가능한 코드&lt;/strong&gt;를 통해 구현된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;상태&quot;&gt;상태&lt;/h2&gt;
&lt;blockquote&gt;
  &lt;p&gt;예로 든 모든 일들의 공통점은 어떤 행동의 결과는 &lt;strong&gt;과거에 어던 행동들이 일어났었느냐에 의존&lt;/strong&gt;한다는 것이다. (p.47)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;인간은 행동의 과정과 결과를 단순하게 기술하기 위해 &lt;strong&gt;상태&lt;/strong&gt; 라는 개념을 고안함&lt;ul&gt;
&lt;li&gt;상태를 이용하면, 과거의 모든 행동 이력을 설명하지 않고도 행동의 결과를 쉽게 예측하고 설명 가능&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;프로퍼티&quot;&gt;프로퍼티&lt;/h2&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;li&gt;객체의 상태를 구성하는 모든 특징(단순 값 + 객체)을 통틀어 객체의 &lt;strong&gt;프로퍼티&lt;/strong&gt;라고 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;링크&quot;&gt;링크&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체와 객체 사이의 의미 있는 연결을 의미&lt;ul&gt;
&lt;li&gt;객체 간에는 &lt;strong&gt;링크가 존재해야만&lt;/strong&gt; 요청을 보내고 받을 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;링크는 일반적으로 한 객체가 다른 객체의 식별자를 알고 있는 것으로 표현됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;속성&quot;&gt;속성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;링크와 달리 객체를 구성하는 단순 값을 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;행동&quot;&gt;행동&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;객체는 자율적인 존재로, 다른 객체가 현 객체의 상태에 직접 접근하거나 변경할 수 없다&lt;ul&gt;
&lt;li&gt;객체의 상태를 변경하는 것은 객체의 자발적인 &lt;strong&gt;행동&lt;/strong&gt;뿐이며 이를 통해 객체의 자율성을 유지한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;객체가 취하는 행동은 &lt;strong&gt;객체 자신의 상태를 변경&lt;/strong&gt;시킨다&lt;ul&gt;
&lt;li&gt;이는 행동이 &lt;strong&gt;부수 효과(side effect)&lt;/strong&gt;를 초래한다는 것을 의미한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;상태와-행동-사이의-관계&quot;&gt;상태와 행동 사이의 관계&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체의 행동의 결과는 상태에 영향을 받고&lt;/li&gt;
&lt;li&gt;객체의 행동은 상태를 변경시킨다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;행동과-협력&quot;&gt;행동과 협력&lt;/h3&gt;
&lt;ul&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;li&gt;행동 내에서 협력하는 다른 객체에 대한 메시지 전송&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;상태-캡슐화&quot;&gt;상태 캡슐화&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;객체지향의 세계에서 모든 객체는 자신의 상태를 스스로 관리하는 자율적인 존재&lt;ul&gt;
&lt;li&gt;현실 세계의 객체와 객체지향 세계의 객체 사이의 중요한 차이점&lt;/li&gt;
&lt;li&gt;ex) 앨리스가 마신 음료의 양을 줄이는 것은 &lt;strong&gt;음료 자신이어야 함&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;객체는 상태를 캡슐 안에 감춰둔 채 외부로 노출하지 않는다 (캡슐화)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;외부로는 &lt;strong&gt;행동만을 노출&lt;/strong&gt;하고 행동을 통해서만 접근이 가능하도록 한다&lt;/li&gt;
&lt;li&gt;객체에 메시지를 보내는 객체는 &lt;strong&gt;어떤 상태가 어떻게 변경되는지&lt;/strong&gt; 전혀 알지 못한다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;메시지를 수신한 객체는 상태를 변경할지, 어떻게 변경할지 &lt;strong&gt;스스로 결정한다&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;송신자가 상태 변경을 기대하더라도 수신자가 자신의 상태를 변경하지 않는다면, 송신자가 간섭할 수 있는 어떤 여지도 없다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;객체의 자율성이 높아질수록 객체의 지능도 높아진다 -&gt; 협력도 유연하고 간결해진다&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;식별자&quot;&gt;식별자&lt;/h2&gt;
&lt;h3 id=&quot;동등성-equality&quot;&gt;동등성 (equality)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;상태를 이용&lt;/strong&gt;해 두 값이 같은지 판단할 수 있는 성질&lt;ul&gt;
&lt;li&gt;ex) 값(value)은 상태를 비교해 같은지를 판단&lt;/li&gt;
&lt;li&gt;값의 상태가 변하지 않기 때문에 상태를 이용해 동등성을 판단할 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;동일성-identical&quot;&gt;동일성 (identical)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체는 시간에 따라 변경되는 가변 상태를 포함한다&lt;ul&gt;
&lt;li&gt;따라서, 특정 시점에 두 객체의 상태가 완전히 똑같더라도 두 객체는 별개의 객체로 다뤄야 함&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;두 객체의 상태가 다르더라도 &lt;strong&gt;식별자가 같다면&lt;/strong&gt; 두 객체를 같은 객체로 판단할 수 있다 -&amp;gt; 동일성&lt;ul&gt;
&lt;li&gt;식별자는 상태 변경에 독립적&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;기계로서의-객체&quot;&gt;기계로서의 객체&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;쿼리 (query) - 객체의 상태를 조회하는 작업&lt;/li&gt;
&lt;li&gt;명령(command) - 객체의 상태를 변경하는 작업&lt;/li&gt;
&lt;li&gt;기계는 &lt;strong&gt;버튼으로 제공되는 행동&lt;/strong&gt;과 &lt;strong&gt;디스플레이에 출력되는 상태&lt;/strong&gt;를 함께 가진다&lt;/li&gt;
&lt;li&gt;기계의 부품은 금속 안에 감춰져 있기 때문에 기계를 분해하지 않는 한 기계의 내부를 직접 볼 수 없다&lt;ul&gt;
&lt;li&gt;사람은 기계 외부의 버튼(행동, 메시지)을 통해서만 기계와 상호작용할 수 있다&lt;/li&gt;
&lt;li&gt;사람이 버튼을 누른다고 &lt;strong&gt;사람이 원하는 대로 상태가 변화하지 않는다&lt;/strong&gt;, 어떻게 상태를 바꿀 지는 기계가 알아서 판단한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;행동이-상태를-결정한다&quot;&gt;행동이 상태를 결정한다&lt;/h2&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체지향에 갓 입문한 사람들이 가장 쉽게 빠지는 함정은 상태를 중심으로 객체를 바라보는 것이다. 초보자들은 먼저 객체에 필요한 상태가 무엇인지를 결정하고 그 상태에 필요한 행동을 결정한다. (중략) 안타깝게도 상태를 먼저 결정하고 행동을 나중에 결정하는 방법은 설계에 나쁜 영향을 끼친다. (p.64)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;상태를-먼저-고려하면&quot;&gt;상태를 먼저 고려하면&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;캡슐화가 저해된다&lt;ul&gt;
&lt;li&gt;상태가 API를 통해 노출될 확률이 높아진다&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;/ul&gt;
&lt;h3 id=&quot;행동책임을-먼저-고려하라&quot;&gt;행동(책임)을 먼저 고려하라&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체의 행동은 객체가 협력에 참여하는 유일한 방법&lt;ul&gt;
&lt;li&gt;따라서 &lt;strong&gt;객체가 적합한지를 결정하는 것&lt;/strong&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;객체지향 설계는 1. 필요한 협력을 생각하고 2. 협력에 필요한 행동을 생각한 후 3. 행동을 수행할 객체를 선택하는 방식으로 진행&lt;ul&gt;
&lt;li&gt;행동을 결정한 이후에 -&amp;gt; 행동에 필요한 정보가 무엇인지 (즉, 어떤 상태가 필요한지) 결정됨&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;애플리케이션에서 &lt;code&gt;객체의 행동 == 객체가 협력에서 완수해야할 책임&lt;/code&gt; 이므로&lt;ul&gt;
&lt;li&gt;결과적으로 어떤 책임이 필요한가를 결정하는 과정이 전체 설계를 주도해야 함&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;의인화&quot;&gt;의인화&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;객체지향 세계는 현실 세계의 단순한 모방(혹은 단순화, 추상화)이 아니다&lt;ul&gt;
&lt;li&gt;소프트웨어 상품은 실제 세계의 상품이 하지 못하는 가격 계산과 같은 행동을 &lt;strong&gt;스스로&lt;/strong&gt; 할 수 있다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;현실 속에서는 수동적인 존재가 소프트웨어 객체로 구현될 때는 능동적으로 변한다&lt;ul&gt;
&lt;li&gt;현실의 객체보다 더 많은 일을 할 수 있는 소프트웨어 객체의 특징을 &lt;strong&gt;의인화&lt;/strong&gt;라고 한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;모든 생물처럼 소프트웨어는 태어나고, 삶을 영위하고, 그리고 죽는다&lt;/code&gt; (Wirfs-Brock 1990)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;은유&quot;&gt;은유&lt;/h3&gt;
&lt;ul&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;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;우리의 목적은 현실을 모방하는 것이 아니다. 단지 이상한 나라를 창조하기만 하면 된다. 현실을 닮아야 한다는 어떤 제약이나 구속도 없다. (p.71)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;질문&quot;&gt;질문&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;p.50 - 프로퍼티와 프로퍼티 값의 차이&lt;/li&gt;
&lt;li&gt;p.57 - 값의 상태는 결코 변하지 않는다 -&amp;gt; 값은 상수를 의미하는지? 리터럴?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;이 게시글은 스터디에서 [ &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001628109&quot;&gt;객체지향의 사실과 오해 / 조영호&lt;/a&gt; ] 책을 읽고 중요한 내용을 잊지 않기 위해 정리한 게시글입니다. 요약 및 생략된 내용이 많고 제가 이해한 대로 다시 정리한 내용이라 보다 자세하고 정확한 설명은 책 구매를 권장합니다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/85</guid>
      <comments>https://gmelon.dev/85#entry85comment</comments>
      <pubDate>Mon, 20 Mar 2023 14:51:23 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘 - DP] 프로그래머스 - 등굣길</title>
      <link>https://gmelon.dev/82</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42898&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;h3 id=&quot;문제-이해&quot;&gt;문제 이해&lt;/h3&gt;
&lt;p&gt;아래와 같은 &lt;strong&gt;m x n&lt;/strong&gt; 크기의 격자가 있을 때,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FJ6Qj/btr37vu6IjD/imXtveCHIKL6817qY5Xkk0/img.png&quot; alt=&quot;image-20230316002515527&quot; /&gt;&lt;/p&gt;
&lt;p&gt;가장 왼쪽 위 (1, 1) 에서 가장 우측 아래쪽 (m, n) 으로 가는 방법의 개수를 구하는 문제이다. (1,000,000,007로 나눈 나머지 반환) 단, 이때 위 그림에서처럼 웅덩이가 있는 곳은 지나가지 못한다.&lt;/p&gt;
&lt;p&gt;입력으로는 격자의 크기 &lt;code&gt;m&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt;과 웅덩이의 좌표가 &lt;code&gt;[[2, 2]]&lt;/code&gt;와 같은 형태로 &lt;code&gt;puddles&lt;/code&gt; 이름으로 제공된다. &lt;/p&gt;
&lt;h3 id=&quot;풀이-방법&quot;&gt;풀이 방법&lt;/h3&gt;
&lt;p&gt;문제에서 우측 및 아래로 움직이는 경로만 가능하다고 했으므로 (1, 1)에서 시작하여 (m, n)까지 우측 및 아래로 한 칸씩 이동하며 경로의 개수를 계산한다. 이때, 예를 들어 (3, 3)까지의 경로 경우의 수는 아래와 같이 (2, 3) 까지의 경로 경우의 수와 (3, 2) 까지의 경로 경우의 수를 더한 값과 같다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PLEPQ/btr36GKumS4/HKG3JVXHBF94w6XWUKlMs0/img.png&quot; alt=&quot;image-20230315235440998&quot; /&gt;&lt;/p&gt;
&lt;p&gt;이런식으로 계속 반복하다보면 (m, n) 위치의 경우의 수도 아래와 같이 구할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3rQyV/btr34ChIafH/Tk30xqKCp9gD1poPFLJ7Ck/img.png&quot; alt=&quot;image-20230315235543413&quot; /&gt;&lt;/p&gt;
&lt;p&gt;따라서 점화식을 세워보면, &lt;code&gt;answer[m, n] = answer[m - 1][n] + answer[m][n-1]&lt;/code&gt; 이라 할 수 있다.&lt;/p&gt;
&lt;p&gt;이제 위 점화식을 사용해 코드를 작성하면 되는데 아래 두 가지를 고려해야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;웅덩이에 접근하는 경로(puddles)는 사용하지 않을 것&lt;/li&gt;
&lt;li&gt;한 번 구한 경로는 다시 계산하지 않도록 메모이제이션을 적용할 것&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위 점들을 고려해 재귀로 작성한 코드는 아래와 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;class Solution {
    public int solution(int m, int n, int[][] puddles) {
        int[][] answer = new int[m + 1][n + 1];        

        // 물에 잠긴 지역 별도 배열로 생성
        int[][] processedPuddles = new int[m + 1][n + 1];
        for (int i = 0 ; i &amp;lt; puddles.length ; i++) {
            processedPuddles[puddles[i][0]][puddles[i][1]] = -1;
        }

        // 재귀 호출
        return search(m, n, m, n, answer, processedPuddles);
    }

    public int search(int m, int n,
                       int x, int y,
                       int[][] answer, int[][] processedPuddles) {
        // 초기 케이스
        if (x == 1 &amp;amp;&amp;amp; y == 1) {
            return 1;
        }
        // 범위를 벗어나면 제외
        if (x &amp;gt; m || x &amp;lt; 1 || y &amp;gt; n || y &amp;lt; 1) {
            return 0;
        }
        // 물에 잠긴 지역은 제외
        if (processedPuddles[x][y] == -1) 
            return 0;

        // 메모이제이션
        if (answer[x][y] != 0) {
            return answer[x][y];
        }

        // 나머지 정상 케이스
        return answer[x][y] = (search(m, n, x - 1, y, answer, processedPuddles) 
                               + search(m, n, x, y - 1, answer, processedPuddles)) 
                                % 1_000_000_007;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;근데 뭔가 짜고 보니 재귀보다 반복이 더 보기 좋을 것 같아서 다시 작성했다. ㅎㅎ&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;java language-java&quot;&gt;class Solution {
    public int solution(int m, int n, int[][] puddles) {
        int[][] answer = new int[m + 1][n + 1];

        // 물에 잠긴 지역 처리
        for (int i = 0 ; i &amp;lt; puddles.length ; i++) {
            answer[puddles[i][0]][puddles[i][1]] = -1;
        }

        // 초기 좌표 값 설정
        answer[1][1] = 1;

        // 순회하며 경우의 수 계산
        for (int x = 1 ; x &amp;lt;= m ; x++) {
            for (int y = 1 ; y &amp;lt;= n ; y++) {
                // 물에 잠긴 지역 제외
                if (answer[x][y] == -1) {
                    answer[x][y] = 0;
                    continue;
                }

                // 나머지 정상 경로 계산
                if (x != 1) {
                    answer[x][y] += answer[x - 1][y] % 1_000_000_007;
                }
                if (y != 1) {
                    answer[x][y] += answer[x][y - 1] % 1_000_000_007;
                }
            }
        }
        return answer[m][n] % 1_000_000_007;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드에서 주의해야 할 부분들은 아래와 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;계산을 모두 마치고 반환할 때만 1,000,000,007로 나눈 나머지를 반환하는 것과 매번 나누고 더해주는 것의 값 차이는 없으나 후자의 방법을 사용해야 효율성 테스트에서 탈락하지 않는다.&lt;/li&gt;
&lt;li&gt;물에 잠긴 지역을 처리할 때 &lt;strong&gt;순회&lt;/strong&gt; 방식으로 하게 되면 &lt;strong&gt;물에 잠긴 지역을 단 한 번만 방문&lt;/strong&gt;하기 때문에 굳이 별도의 배열을 사용하지 않고 &lt;code&gt;answer&lt;/code&gt; 배열에 &lt;code&gt;-1&lt;/code&gt; 등으로 저장해두고 해당 좌표를 지나갈 때 &lt;code&gt;0&lt;/code&gt;으로 값을 바꿔주면서 &lt;code&gt;continue&lt;/code&gt; 처리해주면 해당 좌표를 경로로 사용하지 않도록 처리가 가능하다.&lt;/li&gt;
&lt;li&gt;초기 좌표 값에 위치하는 경우의 수는 한 가지이므로 &lt;code&gt;answer[1][1]&lt;/code&gt;은 &lt;code&gt;1&lt;/code&gt;로 초기화해준다.&lt;/li&gt;
&lt;li&gt;순회로 풀이하는 경우 x가 1가 아닐 때 좌측 좌표의 경우의 수를 현재 좌표에 더해주고, y가 1이 아닐 때 위쪽 좌표의 경우의 수를 현재 좌표에 더해준다.&lt;/li&gt;
&lt;/ol&gt; &lt;/article&gt;</description>
      <category>개발 공부/알고리즘</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/82</guid>
      <comments>https://gmelon.dev/82#entry82comment</comments>
      <pubDate>Thu, 16 Mar 2023 00:25:32 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향의 사실과 오해] 1장 - 협력하는 객체들의 공동체</title>
      <link>https://gmelon.dev/80</link>
      <description>&lt;article class=&quot;markdown-body entry-content&quot; itemprop=&quot;text&quot;&gt; &lt;h2 id=&quot;서론&quot;&gt;서론&lt;/h2&gt;
&lt;h3 id=&quot;객체지향으로-향하는-4가지-걸음&quot;&gt;객체지향으로 향하는 4가지 걸음&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;li&gt;위 개념들을 사용하는 프로그래밍 언어라는 틀에 잘 담아내는 기술을 익히는 것&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;1장&quot;&gt;1장&lt;/h2&gt;
&lt;blockquote&gt;
  &lt;p&gt;실세계의 모방이라는 개념은 객체지향의 기반을 이루는 철학적인 개념을 설명하는 데는 적합하지만 유연하고 실용적인 관점에서 객체지향 분석, 설계를 설명하기에는 적합하지 않다. (중략) 객체지향의 목표는 실세계를 모방하는 것이 아니다. 오히려 새로운 세계를 창조하는 것이다. 소프트웨어 개발자의 역할은 단순히 실세계를 소프트웨어 안으로 옮겨 담는 것이 아니라 고객과 사용자를 만족시킬 수 있는 신세계를 창조하는 것이다. (p.20-21)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;요청과-응답으로-구성된-협력&quot;&gt;요청과 응답으로 구성된 협력&lt;/h3&gt;
&lt;blockquote&gt;
  &lt;p&gt;먼저 소프트웨어 객체란 실세계 사물의 모방이라는 전통적인 관점에서 객체지향의 다양한 개념을 설명하기로 한다. (p.22)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;대부분의 문제는 개인 혼자만의 힘으로 해결하기 어려움&lt;ul&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;ul&gt;
&lt;li&gt;즉, &lt;strong&gt;응답&lt;/strong&gt;을 수행&lt;/li&gt;
&lt;li&gt;응답은 요청의 방향과 반대 방향으로 &lt;strong&gt;연쇄적으로 전달&lt;/strong&gt;됨&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;요청과 응답을 통해 &lt;strong&gt;협력&lt;/strong&gt;하는 것은 거대하고 복잡한 문제를 해결할 수 있게 만드는 기반임&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;역할-책임-협력&quot;&gt;역할, 책임, 협력&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;역할 - 어떤 협력에 참여하는 특정한 사람이 &lt;strong&gt;협력 안에서 차지하는 책임이나 임무&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;ex) 손님 - 커피를 주문하는 임무, 캐시어 - 주문을 받는 임무, 바리스타 - 주문된 커피를 제조할 &lt;strong&gt;책임&lt;/strong&gt; (또는 역할)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;역할은 의미적으로 책임이라는 개념을 내포.&lt;ul&gt;
&lt;li&gt;즉, 특정한 역할은 특정한 책임을 &lt;strong&gt;암시&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;사람들은 협력을 위해 특정한 역할을 맡고, &lt;strong&gt;역할에 적합한 책임&lt;/strong&gt;을 수행한다. 역할과 책임은 &lt;strong&gt;협력이 원활하게 진행되는 데 필요&lt;/strong&gt;한 핵심 구성 요소이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;하나의 역할은 &lt;strong&gt;여러 사람에 의해 수행&lt;/strong&gt;될 수 있으며 따라서 &lt;strong&gt;대체가 가능&lt;/strong&gt;(substitutable)하다&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;책임을 수행하는 방법&lt;/strong&gt;은 자율적으로 선택할 수 있다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ex) 커피를 만들&lt;strong&gt;(책임)&lt;/strong&gt;더라도 바리스타마다 서로 다른 방법&lt;strong&gt;(서로 다른 방식)&lt;/strong&gt;으로 커피를 제조할 수 있다.&lt;/li&gt;
&lt;li&gt;동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 능력을 &lt;strong&gt;다형성&lt;/strong&gt;(polymorphism) 이라고 한다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;한 사람이 &lt;strong&gt;동시에 여러 역할&lt;/strong&gt;을 수행할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ex) 캐시어 + 바리스타&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;협력&lt;/strong&gt;의 핵심 - 특정한 책임을 수행하는 역할들 간의 &lt;strong&gt;연쇄적인 요청과 응답&lt;/strong&gt;을 통해 목표를 달성한다는 것&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;목표 -&amp;gt; 더 작은 책임으로 분할&lt;/li&gt;
&lt;li&gt;각 책임 -&amp;gt; 해당 책임을 수행할 수 있는 적절한 역할을 가진 사람(객체)에 의해 수행&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체지향 설계라는 예술은 적절한 객체에게 적절한 책임을 할당하는 것에서 시작된다. 책임은 객체지향 설계의 품질을 결정하는 가장 중요한 요소다. (p.30)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;역할 - 협력에 참여하는 객체에 대한 일종의 페르소나&lt;ul&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;/ul&gt;
&lt;h3 id=&quot;협력-속에-사는-객체&quot;&gt;협력 속에 사는 객체&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체는 &lt;strong&gt;협력적&lt;/strong&gt;이어야 한다&lt;ul&gt;
&lt;li&gt;다른 객체의 요청에 귀 기울이고, 다른 객체에게 적극적으로 도움을 요청해야 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스스로 모든 것을 처리&lt;/strong&gt;하려고 하는 전지전능한 객체 (god object)는 &lt;strong&gt;내부적인 복잡도의 의해 자멸&lt;/strong&gt;하고 만다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;객체는 &lt;strong&gt;자율적&lt;/strong&gt;이어야 한다&lt;ul&gt;
&lt;li&gt;객체는 스스로의 결정과 판단에 따라 행동하는 자율적인 존재여야 한다&lt;/li&gt;
&lt;li&gt;ex) 손님이 캐시어에게 어떤 방식으로 주문을 받고 어떻게 바리스타에게 주문을 전달할지까지 명령하지 않는다. 손님은 단지 &lt;strong&gt;주문을 하고 음료를 받기를 기대할 뿐&lt;/strong&gt;이다.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체지향 설계의 묘미는 다른 객체와 조화롭게 협력할 수 있을 만큼 충분히 개방적인 동시에 협력에 참여하는 방법을 스스로 결정할 수 있을 만큼 충분히 자율적인 객체들의 공동체를 설계하는 데 있다.(p.32)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;상태와-행동&quot;&gt;상태와 행동&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체가 스스로 판단하고 결정하는 자율적인 존재로 남기 위해선 필요한 &lt;strong&gt;행동과 상태&lt;/strong&gt;를 함께 지니고 있어야 한다&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;객체&lt;/strong&gt;를 통해 상태와 행위를 하나로 묶음으로써 유지보수가 쉽고 재사용이 용이한 시스템을 구축할 수 있게 된다&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;외부에서는 현 객체가 무엇(what)을 수행하는지만 알고, 어떻게(how) 수행하는지는 몰라야 한다&lt;ul&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;/ul&gt;
&lt;h3 id=&quot;메시지&quot;&gt;메시지&lt;/h3&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 id=&quot;메서드&quot;&gt;메서드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;객체가 수신된 메시지를 처리하는 방법&lt;ul&gt;
&lt;li&gt;메시지를 수신한 객체가 런타임에 실행할 메서드를 선택할 수 있다는 점은 객체지향 언어의 핵심 특징&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메시지&lt;/strong&gt;와 &lt;strong&gt;메서드&lt;/strong&gt;는 다르다!!&lt;ul&gt;
&lt;li&gt;메시지와 메서드를 분리하는 것은 객체들 간의 자율성을 증진시키는 핵심 매커니즘이다&lt;/li&gt;
&lt;li&gt;이는 캡슐화(encapsulation)과도 깊이 연관되어 있다&lt;/li&gt;
&lt;li&gt;ex) 캐시어가 바리스타에게 커피 제조를 요청(&lt;strong&gt;메시지&lt;/strong&gt;)하면 바리스타는 1. 커피머신을 사용해서, 2. 커피를 직접 필터를 거쳐 내려서 (&lt;strong&gt;구체적인 방법 - 메서드&lt;/strong&gt;) 커피를 제조한다. 캐시어는 바리스타의 구체적인 커피 제조 방법에 관여하지 않는다!&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;객체지향의-본질&quot;&gt;객체지향의 본질&lt;/h3&gt;
&lt;blockquote&gt;
  &lt;p&gt;객체지향이란 시스템을 상호작용하는 &lt;strong&gt;자율적인 객체들의 공동체&lt;/strong&gt;로 바라보고 객체를 이용해 &lt;strong&gt;시스템을 분할&lt;/strong&gt;하는 방법이다. (p.35)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
  &lt;p&gt;클래스에 대한 중요성이 과하다 싶을 정도로 강조됐다. (중략) 그 결과 사람들은 객체지향의 중심에 있어야 할 객체로부터 조금씩 멀어져 갔다. (p.37)&lt;/p&gt;
  &lt;p&gt;어떤 객체지향 프로그래밍 언어를 이야기할 때 대부분의 사람들은 클래스를 정의하는 방법과 클래스 사이의 상속에 초점을 맞춘다. (중략) 지나치게 클래스를 강조하는 프로그래밍 언어적인 관점은 객체의 캡슐화를 저해하고 클래스를 서로 강하게 결합시킨다. (p.38)&lt;/p&gt;
  &lt;p&gt;훌륭한 객체지향 설계자가 되기 위해 거쳐야 할 첫 번째 도전은 코드를 담는 클래스의 관점에서 메시지를 주고받는 객체의 관점으로 사고의 중심을 전환하는 것이다. &lt;strong&gt;중요한 것은 어떤 클래스가 필요한가가 아니라 어떤 객체들이 어떤 메시지를 주고받으며 협력하는가다.&lt;/strong&gt; (p.38)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;객체의 역할, 책임, 협력에 집중하기&lt;ul&gt;
&lt;li&gt;클래스는 단지 객체를 구현하는 매커니즘일 뿐&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;핵심은 &lt;strong&gt;적절한 책임&lt;/strong&gt;을 수행하는 &lt;strong&gt;역할 간&lt;/strong&gt;의 &lt;strong&gt;유연하고 견고한 협력 관계&lt;/strong&gt;를 구축하는 것!&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;이 게시글은 스터디에서 [ &lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001628109&quot;&gt;객체지향의 사실과 오해 / 조영호&lt;/a&gt; ] 책을 읽고 중요한 내용을 잊지 않기 위해 정리한 게시글입니다. 요약 및 생략된 내용이 많고 제가 이해한 대로 다시 정리한 내용이라 보다 자세하고 정확한 설명은 책 구매를 권장합니다.&lt;/p&gt; &lt;/article&gt;</description>
      <category>스터디/자바</category>
      <author>gmelon</author>
      <guid isPermaLink="true">https://gmelon.dev/80</guid>
      <comments>https://gmelon.dev/80#entry80comment</comments>
      <pubDate>Mon, 13 Mar 2023 17:29:52 +0900</pubDate>
    </item>
  </channel>
</rss>