<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>tt</title>
    <link>https://oxdjww.tistory.com/</link>
    <description>안녕.하십니까.</description>
    <language>ko</language>
    <pubDate>Tue, 16 Jun 2026 01:41:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>oxdjww</managingEditor>
    <image>
      <title>tt</title>
      <url>https://tistory1.daumcdn.net/tistory/6317606/attach/4e3d5780322e478fbea23642693bcaf1</url>
      <link>https://oxdjww.tistory.com</link>
    </image>
    <item>
      <title>[Focussu] Kafka &amp;amp; Redis 통합/단위 테스트</title>
      <link>https://oxdjww.tistory.com/94</link>
      <description>&lt;h2 data-end=&quot;110&quot; data-start=&quot;100&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 들어가며&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;228&quot; data-start=&quot;111&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;최근 집중도 분석 독서실 플랫폼(또는 스터디룸 플랫폼) 백엔드(Spring Boot)에서, 사용자의 WebRTC 연결 상태를 Kafka 메시지로 수신하고 이를 Redis에 반영하는 기능을 개발했다. 요약하자면,&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;572&quot; data-start=&quot;230&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;334&quot; data-start=&quot;230&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Kafka Topic&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;334&quot; data-start=&quot;252&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;291&quot; data-start=&quot;252&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;mediasoup.user.connected (사용자 입장)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;334&quot; data-start=&quot;294&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;mediasoup.user.disconnected (사용자 퇴장)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;480&quot; data-start=&quot;336&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;동작 흐름&lt;/b&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;480&quot; data-start=&quot;352&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;388&quot; data-start=&quot;352&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka 메시지를 @KafkaListener가 수신&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;428&quot; data-start=&quot;391&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JSON 파싱 후, roomId, userId 추출&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;480&quot; data-start=&quot;431&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis에 roomId를 키로 한 Set 구조에 userId 추가/삭제&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-end=&quot;572&quot; data-start=&quot;482&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;테스트 목표&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;572&quot; data-start=&quot;499&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;572&quot; data-start=&quot;499&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 흐름이 실제로 잘 동작하는지, 즉 &lt;b&gt;Kafka 메시지를 소비했을 때 Redis가 제대로 갱신&lt;/b&gt;되는지를 테스트하고 싶었다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;660&quot; data-start=&quot;574&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그런데 이게 말처럼 간단하지 않았다. 아래에서 어떤 과정을 거쳐 해결했는지, 그리고 왜 여러 가지 대안을 시도해야만 했는지 트러블슈팅 과정을 공유해 본다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-end=&quot;665&quot; data-start=&quot;662&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;714&quot; data-start=&quot;667&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 통합 테스트를 시도하다: Kafka Embedded &amp;amp; Redis 임베디드&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;738&quot; data-start=&quot;716&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.1 Kafka Embedded&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;847&quot; data-start=&quot;739&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka의 경우, spring-kafka-test에서 제공하는 @EmbeddedKafka 애노테이션을 사용하면 손쉽게 임베디드 브로커를 띄울 수 있다. 예를 들어 아래와 같이 작성한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&lt;br /&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744032034718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = {
    &quot;mediasoup.user.connected&quot;,
    &quot;mediasoup.user.disconnected&quot;
})
@ActiveProfiles(&quot;test&quot;)
class StudyRoomParticipantKafkaIntegrationTest {

    @Autowired
    private KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate;

    @Autowired
    private StudyRoomParticipantQueryService queryService;

    @Test
    void testKafkaConnected_addsUserToRedis() throws Exception {
        // given
        String message = &quot;{\&quot;roomId\&quot;:1, \&quot;userId\&quot;:\&quot;user123\&quot;}&quot;;

        // when
        kafkaTemplate.send(&quot;mediasoup.user.connected&quot;, message);

        // then
        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -&amp;gt; {
            Set&amp;lt;String&amp;gt; participants = queryService.getParticipants(1L);
            assertThat(participants).contains(&quot;user123&quot;);
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1817&quot; data-start=&quot;1686&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1743&quot; data-start=&quot;1686&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;@EmbeddedKafka 설정 후, kafkaTemplate을 통해 메시지를 전송한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1817&quot; data-start=&quot;1744&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;awaitility 라이브러리를 사용해 메시지 처리 지연이 있어도 일정 시간 안에 Redis 데이터가 준비되었는지 체크한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1865&quot; data-start=&quot;1819&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka는 이 방식이 꽤 &lt;b&gt;안정적으로 동작&lt;/b&gt;한다. 문제는 Redis 쪽이었다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;1891&quot; data-start=&quot;1867&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.2 Redis 임베디드 도입 시도&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2028&quot; data-start=&quot;1892&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis도 비슷하게 &amp;ldquo;embedded-redis&amp;rdquo; 라이브러리를 사용해 &lt;b&gt;메모리 상에서 Redis를 띄워&lt;/b&gt; 테스트 환경을 구성하려고 했다. 그러나 macOS(M1 칩 등 ARM 기반) + JDK 17 이상의 환경에서 호환성 이슈가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744032045118&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public RedisServer redisServer() {
    return new RedisServer(6379); // 여기서 에러 발생
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2242&quot; data-start=&quot;2133&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2186&quot; data-start=&quot;2133&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Caused by: java.net.UnknownHostException: dummy&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2242&quot; data-start=&quot;2187&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Can't start redis server. Check logs for details.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2374&quot; data-start=&quot;2244&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;등등, 여러 오류가 발생했다. 특히 &amp;ldquo;dummy&amp;rdquo; 호스트를 사용해 테스트 컨테이너에서 오버라이드하려 했는데도, 로컬 환경이 잡히지 않거나 애초에 embedded-redis가 &lt;b&gt;ARM 환경을 제대로 지원하지 않는&lt;/b&gt; 문제가 컸다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-end=&quot;2379&quot; data-start=&quot;2376&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2420&quot; data-start=&quot;2381&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. 대안 1: Mock Redis(lettuce-mock) 활용&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;2460&quot; data-start=&quot;2422&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3.1 lettuce-mock을 이용한 Redis 독립성 확보&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2601&quot; data-start=&quot;2461&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis를 실제로 띄우지 않고, &lt;b&gt;Mock 라이브러리를 사용&lt;/b&gt;하는 방법이다. spring-data-redis-mock과 lettuce-mock 조합을 사용하면, 테스트 환경에서 RedisTemplate이 동작하는 것처럼 흉내 낼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;2614&quot; data-start=&quot;2603&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;의존성 예시&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744032076449&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;testImplementation 'io.github.vanroy:spring-data-redis-mock:2.0.0'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;2707&quot; data-start=&quot;2697&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설정 파일&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;pre id=&quot;code_1744032070466&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class MockRedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceMockConnectionFactory();
    }

    @Bean
    public RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate(
        RedisConnectionFactory factory
    ) {
        RedisTemplate&amp;lt;String, String&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(factory);
        return template;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3290&quot; data-start=&quot;3163&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이렇게 설정하면, 애플리케이션의 RedisTemplate은 &lt;b&gt;실제 Redis가 아닌 mock 환경&lt;/b&gt;에서 동작하게 된다. 덕분에 macOS M1 등 특정 OS나 JDK 버전에 구애받지 않고 테스트를 안정적으로 진행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;3309&quot; data-start=&quot;3292&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3.2 통합 테스트 예시&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;3395&quot; data-start=&quot;3310&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이제 Kafka 메시지를 실제로 임베디드 브로커를 통해 보내고, Redis는 mock 환경으로 처리하는 식의 &lt;b&gt;하이브리드 통합 테스트&lt;/b&gt;가 가능해진다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744032093035&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = {
    &quot;mediasoup.user.connected&quot;,
    &quot;mediasoup.user.disconnected&quot;
})
@ActiveProfiles(&quot;test&quot;)
@Import(MockRedisConfig.class)
class StudyRoomParticipantKafkaIntegrationTest {

    @Autowired
    private KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate;

    @Autowired
    private StudyRoomParticipantQueryService queryService;

    @Test
    void testKafkaConnected_addsUserToRedis() throws Exception {
        // given
        String message = &quot;{\&quot;roomId\&quot;:1, \&quot;userId\&quot;:\&quot;user123\&quot;}&quot;;

        // when
        kafkaTemplate.send(&quot;mediasoup.user.connected&quot;, message);

        // then
        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -&amp;gt; {
            Set&amp;lt;String&amp;gt; participants = queryService.getParticipants(1L);
            assertThat(participants).contains(&quot;user123&quot;);
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4379&quot; data-start=&quot;4265&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4329&quot; data-start=&quot;4265&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka 브로커는 실제로 구동(@EmbeddedKafka)되지만, Redis는 mock으로 대체된 상태다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;4379&quot; data-start=&quot;4330&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;ldquo;통합 테스트&amp;rdquo;에 가깝게 진행하면서도, Redis 띄우기 실패 문제에서 자유로워진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;4384&quot; data-start=&quot;4381&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;4431&quot; data-start=&quot;4386&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. 대안 2: 완전한 단위 테스트(RedisTemplate Mocking)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;4613&quot; data-start=&quot;4432&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;또 다른 방법은 &lt;b&gt;Kafka&lt;/b&gt;와 &lt;b&gt;Redis&lt;/b&gt; 양쪽 모두를 실제로 띄우지 않고, 완전히 &amp;ldquo;단위 테스트&amp;rdquo;로 전환하는 것이다. 특히 Kafka 리스너가 실제로 메시지를 받고 Redis를 갱신하는 로직을 검증할 때, &amp;ldquo;메시지 처리 로직&amp;rdquo; 자체만 집중하고 싶다면 &lt;b&gt;RedisTemplate을 직접 mock&lt;/b&gt;할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4721&quot; data-start=&quot;4615&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아래는 &lt;b&gt;최종적으로&lt;/b&gt; 선택한 테스트 코드 예시다. 여기서는 RedisTemplate과 SetOperations를 mock 처리하여, Redis 연결 없이도 유효한 단위 테스트가 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744032105050&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.focussu.backend.studyroomparticipant;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import com.focussu.backend.studyroomparticipant.service.StudyRoomParticipantQueryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;

@ExtendWith(MockitoExtension.class)
public class StudyRoomParticipantKafkaIntegrationTest {

    @Mock
    private RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    @Mock
    private SetOperations&amp;lt;String, String&amp;gt; setOperations;

    private StudyRoomParticipantQueryService queryService;

    @BeforeEach
    public void setup() {
        // redisTemplate.opsForSet() 호출 시 setOperations 목 객체 반환하도록 설정
        when(redisTemplate.opsForSet()).thenReturn(setOperations);
        queryService = new StudyRoomParticipantQueryService(redisTemplate);
    }

    @Test
    public void testGetParticipants() {
        Long roomId = 1L;
        String key = &quot;studyroom:participants:&quot; + roomId;
        Set&amp;lt;String&amp;gt; expectedParticipants = new HashSet&amp;lt;&amp;gt;(Arrays.asList(&quot;user123&quot;));

        // setOperations.members(key) 호출 시 expectedParticipants 반환하도록 설정
        when(setOperations.members(key)).thenReturn(expectedParticipants);

        Set&amp;lt;String&amp;gt; actualParticipants = queryService.getParticipants(roomId);
        assertEquals(expectedParticipants, actualParticipants);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6688&quot; data-start=&quot;6442&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6501&quot; data-start=&quot;6442&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;단위 테스트가 되므로, &lt;b&gt;OS, 포트 충돌, CI 환경&lt;/b&gt; 등 외부 요인에서 완벽히 자유로워진다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;6559&quot; data-start=&quot;6502&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;대신 &amp;ldquo;Kafka &amp;rarr; Redis&amp;rdquo;라는 &lt;b&gt;실제 메시지 플로우&lt;/b&gt;가 재현되는지는 확인하기 어렵다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;6688&quot; data-start=&quot;6560&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;따라서 &lt;b&gt;정말로 전체 흐름을 검증&lt;/b&gt;하고 싶다면 MockRedisConfig(또는 Testcontainers 활용) 같은 &lt;b&gt;부분 통합 테스트&lt;/b&gt;가 낫고, &lt;b&gt;핵심 로직만 빠르게 검증&lt;/b&gt;하고 싶다면 단위 테스트가 효율적이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;6693&quot; data-start=&quot;6690&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;6729&quot; data-start=&quot;6695&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. 그 외 대안: Testcontainers Redis&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;6874&quot; data-start=&quot;6730&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;만약 &lt;b&gt;CI 환경&lt;/b&gt;에서 Docker 구동이 가능하고, &lt;b&gt;진짜 Redis&lt;/b&gt;를 쓰면서도 독립적 테스트 환경을 원한다면, &lt;a href=&quot;https://www.testcontainers.org/&quot; data-end=&quot;6848&quot; data-start=&quot;6799&quot;&gt;Testcontainers&lt;/a&gt;로 Redis를 띄우는 방법도 있다. 이 경우,&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;6965&quot; data-start=&quot;6876&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;6921&quot; data-start=&quot;6876&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Docker가 설치된 환경이면, Redis 컨테이너를 테스트 시점에 실행&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;6943&quot; data-start=&quot;6922&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테스트 종료 후 컨테이너 정리&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;6965&quot; data-start=&quot;6944&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OS나 포트 문제를 자동으로 해결&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;7116&quot; data-start=&quot;6967&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이라는 장점이 있다. 하지만 설정이 약간 더 복잡해지고, 매 테스트마다 Docker 컨테이너를 띄우므로 &lt;b&gt;테스트 속도&lt;/b&gt;가 줄어들 수 있다. 상황에 따라 &lt;b&gt;Mock&lt;/b&gt; vs &lt;b&gt;Testcontainers&lt;/b&gt; vs &lt;b&gt;Local Embedded&lt;/b&gt;를 선택하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-end=&quot;7121&quot; data-start=&quot;7118&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;7134&quot; data-start=&quot;7123&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;6. 마무리하며&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;7228&quot; data-start=&quot;7135&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka와 Redis를 연동해 메시지를 처리하는 시스템에서, &lt;b&gt;테스트&lt;/b&gt;를 어떻게 구성할지 고민하는 과정은 생각보다 쉽지 않다. 이번 트러블슈팅 사례를 요약해보면,&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;7736&quot; data-start=&quot;7230&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;7353&quot; data-start=&quot;7230&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Kafka&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7353&quot; data-start=&quot;7248&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7298&quot; data-start=&quot;7248&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;@EmbeddedKafka를 통한 임베디드 테스트가 비교적 간단하고 안정적이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;7353&quot; data-start=&quot;7302&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;메시지 테스트는 awaitility 같은 라이브러리로 처리 지연에 유연하게 대응.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;7612&quot; data-start=&quot;7355&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Redis&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7612&quot; data-start=&quot;7373&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7429&quot; data-start=&quot;7373&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;embedded-redis&lt;/b&gt;는 OS, JDK 버전 호환성이 까다롭다(특히 M1 Mac).&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;7502&quot; data-start=&quot;7433&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;lettuce-mock&lt;/b&gt;(또는 spring-data-redis-mock)을 쓰면 독립적인 테스트가 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;7552&quot; data-start=&quot;7506&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;조금 더 실제 환경에 가깝게 하려면 &lt;b&gt;Testcontainers&lt;/b&gt; 활용.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;7612&quot; data-start=&quot;7556&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;단위 테스트&lt;/b&gt;로 돌리려면 RedisTemplate 자체를 mock 처리해 빠른 테스트 가능.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;7736&quot; data-start=&quot;7614&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;통합 vs 단위 테스트&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7736&quot; data-start=&quot;7639&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7682&quot; data-start=&quot;7639&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;모든 것을 한 번에 검증하고 싶다면 통합 테스트(+mock Redis)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;7736&quot; data-start=&quot;7686&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;빠르고 환경 독립적인 검증을 원한다면 단위 테스트(MockBean/Mockito 활용)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;7969&quot; data-start=&quot;7738&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결국 운영 환경, CI 파이프라인, 개발 속도 등을 고려해서 &lt;b&gt;적절히 타협점&lt;/b&gt;을 찾는 것이 핵심이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이번 프로젝트에서는 Kafka Embedded를 쓰는 대신, Redis는 &amp;lsquo;목(mock) 처리&amp;rsquo;로 빠르고 안정적인 테스트를 할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;7969&quot; data-start=&quot;7738&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7969&quot; data-start=&quot;7738&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;물론 &lt;b&gt;실제 운영 환경과 거의 동일&lt;/b&gt;한 흐름을 검증해야 한다면, Testcontainers Redis를 통해 더욱 현실에 가까운 통합 테스트를 구현할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;8086&quot; data-start=&quot;7971&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;결론적으로&lt;/b&gt;, &amp;lsquo;Kafka 메시지 &amp;rarr; Redis 업데이트&amp;rsquo;라는 로직을 검증하는 방법은 여러 갈래가 있다. 운영 환경, CI 설정, 개발자의 편의 등을 종합해 &lt;b&gt;가장 실용적인 방법&lt;/b&gt;을 선택하면 된다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Activities/Focussu 개발일지</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/94</guid>
      <comments>https://oxdjww.tistory.com/94#entry94comment</comments>
      <pubDate>Tue, 15 Apr 2025 16:16:24 +0900</pubDate>
    </item>
    <item>
      <title>[Codetree] 마법의 숲 탐색</title>
      <link>https://oxdjww.tistory.com/93</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://www.codetree.ai/ko/frequent-problems/problems/magical-forest-exploration/description&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024 상반기 오후 1번&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수 시간 5시간 소요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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://oxdjww.tistory.com/92&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;왕실의 기사 대결&quot;&lt;/a&gt; 문제 설계할 때처럼 골렘 객체 2차원 배열을 만들까 고민했는데,&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;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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨리 풀려면 초반부 골렘 내려오는 곳을 굉장히 빠르게 짰으면 더 빠르게 끝났을듯.. ?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디버깅 포인트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. visited 배열 초기화 하는 위치 잘 선정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 좌표가 범위를 벗어나지 않는지 체킹하는 함수 꼭 따로 빼자&lt;/p&gt;
&lt;pre id=&quot;code_1744301313120&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return row &amp;gt;= 3 &amp;amp;&amp;amp; row &amp;lt; R + 3 &amp;amp;&amp;amp; column &amp;gt;= 0 &amp;amp;&amp;amp; column &amp;lt; C &amp;amp;&amp;amp; board[row][column] != 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 변수명 잘 보기.. 현재 골렘 위치와 directions 배열 돌리는 골렘 위치 변수 헷갈려서 30분 날렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 수도코드 잘 짜는 것은 항상 도움이 된다. 이번에 수도 코드 잘 짜서 구현 혼자 잘 한듯&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제출 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1744301159704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;
import java.io.*;

public class Main {
    private static int R;
    private static int C;
    private static int K;
    private static Golem[] golemStatus;
    private static int[][] board;
    private static boolean[][] visited;
    private static int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};

    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        R = Integer.parseInt(st.nextToken());
        C = Integer.parseInt(st.nextToken());
        K = Integer.parseInt(st.nextToken());

        // 초기 위치 고려 상위 3행 더
        board = new int[R+3][C];
        visited = new boolean[R+3][C];
        golemStatus = new Golem[K+1];

        int answer = 0;

        for(int golemNumber = 1; golemNumber &amp;lt;= K; golemNumber++) {
            st = new StringTokenizer(br.readLine());
            int initColumn = Integer.parseInt(st.nextToken());
            int wayOut = Integer.parseInt(st.nextToken());

            setGolemOnBoard(golemNumber, initColumn - 1);
            golemStatus[golemNumber] = new Golem(1, initColumn - 1, wayOut);

            // print();
            answer += execute(golemNumber);            
            // print();
        }

        System.out.println(answer);
    }

    private static int execute(int golemNumber) {
        while(true) {
            if(canMoveSouth(golemNumber)) {
                moveSouth(golemNumber);
                continue;
            } else if(canMoveWest(golemNumber)) {
                moveWest(golemNumber);
                continue;
            } else if(canMoveEast(golemNumber)) {
                moveEast(golemNumber);
                continue;
            }
            break;
        }

        // print();

        if(!checkGolemOutOfBoard(golemNumber)) {
            // 골렘이 숲 밖이면 점수합산 X
            clearBoard();
            return 0;
        }

        int result = moveJungryoungToBottom(golemNumber);
        return result;
    }

    private static void clearBoard() {
        for(int i = 0; i &amp;lt; board.length; i++) {
            Arrays.fill(board[i], 0);
        }
    }

    private static boolean checkGolemOutOfBoard(int golemNumber) {
        int centerRow = golemStatus[golemNumber].row;
        int centerColumn = golemStatus[golemNumber].column;
        // System.out.println(golemNumber + &quot;번 골렘의 중앙값: &quot; + centerRow + &quot;, &quot;  +centerColumn);

        for(int i = 0; i &amp;lt; directions.length; i++) {
            int nr = centerRow + directions[i][0];
            int nc = centerColumn + directions[i][1];
            if(!isValidPoint(nr, nc)) {
                // System.out.println(&quot;GOLEM OUT: &quot; + nr + &quot;, &quot; + nc);
                return false;
            }
        }
        return true;
    }

    private static boolean isValidPoint(int row, int column) {
        return row &amp;gt;= 3 &amp;amp;&amp;amp; row &amp;lt; R + 3 &amp;amp;&amp;amp; column &amp;gt;= 0 &amp;amp;&amp;amp; column &amp;lt; C &amp;amp;&amp;amp; board[row][column] != 0;
    }

    private static int moveJungryoungToBottom(int golemNumber) {
        visited = new boolean[R+3][C];

        // System.out.println(&quot;MOVE JUNGRYUNG&quot;);
        int centerRow = golemStatus[golemNumber].row;
        int centerColumn = golemStatus[golemNumber].column;

        Queue&amp;lt;int[]&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
        // 시작
        queue.offer(new int[]{centerRow, centerColumn});

        // System.out.println(golemNumber + &quot;번 골렘 출구: &quot; + (golemStatus[golemNumber].row + directions[golemStatus[golemNumber].wayOut][0]) + &quot;, &quot; + (
        // golemStatus[golemNumber].column + directions[golemStatus[golemNumber].wayOut][1]));

        int maxRow = 0;
        while(!queue.isEmpty()) {
            // 뽑고
            int[] current = queue.poll();
            // 방문 처리
            visited[current[0]][current[1]] = true;
            // System.out.print(&quot;[&quot; + current[0] + &quot;, &quot; + current[1] + &quot;] &quot;);
            // 최대 행값 갱신
            maxRow = Math.max(maxRow, current[0] - 2);
            
            // 현재 칸의 골렘 넘버를 찾는다
            int currentGolemNumber = board[current[0]][current[1]];

            // 동서남북 점검
            for(int i = 0; i &amp;lt; directions.length; i++) {
                int nr = current[0] + directions[i][0];
                int nc = current[1] + directions[i][1];

                int nWayOut = golemStatus[currentGolemNumber].wayOut;
                // System.out.println(&quot;CURRENT: &quot; + current[0] + &quot;, &quot; + current[1]);
                if(isValidPoint(nr, nc) &amp;amp;&amp;amp; !visited[nr][nc]) { // 다음칸이 0이 아니고 보드를 벗어나지 않고며, 방문하지 않은 경우만 
                    if(board[nr][nc] == currentGolemNumber) { // 같은 골렘 내부
                        queue.offer(new int[]{nr, nc});
                        visited[nr][nc] = true;
                    } else if(
                        current[0] == golemStatus[currentGolemNumber].row + directions[nWayOut][0] &amp;amp;&amp;amp;
                        current[1] == golemStatus[currentGolemNumber].column + directions[nWayOut][1]
                    ) { // 다음칸이 같은 골렘 내부가 아니면 현재 위치가 출구일 경우에만 
                        queue.offer(new int[]{nr, nc});
                        visited[nr][nc] = true;
                    }
                    // 그런것도 아니라면 추가 X
                }
            }
        }
        // System.out.println();
        // System.out.println(&quot;적립: &quot; + maxRow);
        return maxRow;
    }

    private static void moveSouth(int golemNumber) {
        // System.out.println(&quot;MOVE SOUTH&quot;);
        int centerRow = golemStatus[golemNumber].row;
        int centerColumn = golemStatus[golemNumber].column;

        // 기존 골렘 위치 삭제
        board[centerRow][centerColumn] = 0;
        for(int i = 0; i &amp;lt; directions.length; i++) {
            board[centerRow + directions[i][0]][centerColumn + directions[i][1]] = 0;
        }

        // 새 위치
        board[centerRow + 1][centerColumn] = golemNumber;
        for(int i = 0; i &amp;lt; directions.length; i++) {
            board[centerRow + 1 + directions[i][0]][centerColumn + directions[i][1]] = golemNumber;
        }

        // 새 위치 반영
        golemStatus[golemNumber].row = centerRow + 1;
    }

    private static void moveWest(int golemNumber) {
        // System.out.println(&quot;MOVE WEST&quot;);
        int centerRow = golemStatus[golemNumber].row;
        int centerColumn = golemStatus[golemNumber].column;

        // 기존 골렘 위치 삭제
        board[centerRow][centerColumn] = 0;
        for(int i = 0; i &amp;lt; directions.length; i++) {
            board[centerRow + directions[i][0]][centerColumn + directions[i][1]] = 0;
        }

        // 새 위치
        board[centerRow + 1][centerColumn - 1] = golemNumber;
        for(int i = 0; i &amp;lt; directions.length; i++) {
            board[centerRow+ 1 + directions[i][0]][centerColumn - 1 + directions[i][1]] = golemNumber;
        }
        
        // 새 위치 반영
        golemStatus[golemNumber].row = centerRow + 1;
        golemStatus[golemNumber].column = centerColumn - 1;

        // 출구 반시계방향으로 이동
        int wayOutResult = golemStatus[golemNumber].wayOut - 1;
        golemStatus[golemNumber].wayOut = wayOutResult &amp;lt; 0 ? 3 : wayOutResult;
    }

    private static void moveEast(int golemNumber) {
        // System.out.println(&quot;MOVE EAST&quot;);
        int centerRow = golemStatus[golemNumber].row;
        int centerColumn = golemStatus[golemNumber].column;

        // 기존 골렘 위치 삭제
        board[centerRow][centerColumn] = 0;
        for(int i = 0; i &amp;lt; directions.length; i++) {
            // System.out.println((centerRow + directions[i][0]) + &quot;, &quot; + (centerColumn + directions[i][1]));

            board[centerRow + directions[i][0]][centerColumn + directions[i][1]] = 0;
        }

        // 새 위치
        board[centerRow + 1][centerColumn + 1] = golemNumber;
        for(int i = 0; i &amp;lt; directions.length; i++) {
            // System.out.println((centerRow + 1 + directions[i][0]) + &quot;, &quot; + (centerColumn + 1 + directions[i][1]));

            board[centerRow + 1 + directions[i][0]][centerColumn + 1 + directions[i][1]] = golemNumber;
        }

        // 새 위치 반영
        golemStatus[golemNumber].row = centerRow + 1;
        golemStatus[golemNumber].column = centerColumn + 1;

        // 출구 시계방향으로 이동
        int wayOutResult = golemStatus[golemNumber].wayOut + 1;
        golemStatus[golemNumber].wayOut = wayOutResult &amp;gt; 3 ? 0 : wayOutResult;
    }

    private static boolean canMoveSouth(int golemNumber) {
        int centerRow = golemStatus[golemNumber].row;
        int centerColumn = golemStatus[golemNumber].column;
        
        // 골렘의 중앙(정령)이 보드 R - 2행에 왔으면
        if(centerRow &amp;gt;= R + 1) return false;

        if(board[centerRow + 1][centerColumn - 1] != 0) return false;
        if(board[centerRow + 2][centerColumn] != 0) return false;
        if(board[centerRow + 1][centerColumn + 1] != 0) return false;

        return true;
    }

    private static boolean canMoveWest(int golemNumber) {
        int centerRow = golemStatus[golemNumber].row;
        int centerColumn = golemStatus[golemNumber].column;
        
        // 골렘의 중앙(정령)이 보드 R - 2행에 왔으면
        if(centerRow &amp;gt;= R + 1) return false;
        // 골렘의 중앙(정령)이 보드 1열에 왔으면
        if(centerColumn &amp;lt;= 1) return false;

        // 남쪽 점검
        if(board[centerRow - 1][centerColumn - 1] != 0) return false;
        if(board[centerRow][centerColumn - 2] != 0) return false;
        if(board[centerRow + 1][centerColumn - 1] != 0) return false;

        // 서쪽 점검
        if(board[centerRow + 1][centerColumn - 2] != 0) return false;
        if(board[centerRow + 2][centerColumn - 1] != 0) return false;

        return true;
    }

    private static boolean canMoveEast(int golemNumber) {
        int centerRow = golemStatus[golemNumber].row;
        int centerColumn = golemStatus[golemNumber].column;
        
        // 골렘의 중앙(정령)이 보드 R - 2행에 왔으면
        if(centerRow &amp;gt;= R + 1) return false;
        // 골렘의 중앙(정령)이 보드 C - 2행에 왔으면
        if(centerColumn &amp;gt;= C - 2) return false;

        // 남쪽 점검
        if(board[centerRow - 1][centerColumn + 1] != 0) return false;
        if(board[centerRow][centerColumn + 2] != 0) return false;
        if(board[centerRow + 1][centerColumn + 1] != 0) return false;

        // 동쪽 점검
        if(board[centerRow + 2][centerColumn + 1] != 0) return false;
        if(board[centerRow + 1][centerColumn + 2] != 0) return false;

        return true;
    }

    private static void setGolemOnBoard(int golemNumber, int initColumn) {
        board[1][initColumn] = golemNumber;
        for(int i = 0; i &amp;lt; directions.length; i++) {
            board[1 + directions[i][0]][initColumn + directions[i][1]] = golemNumber;
        }
    }

    private static void print() {
        for(int i = 0; i &amp;lt; board.length; i++) {
            for(int j = 0; j &amp;lt; board[i].length; j++) {
                System.out.print(board[i][j] + &quot; &quot;);
            }
            System.out.println();
        }
        System.out.println();
    }
}

class Golem {
    int row, column;
    int wayOut;

    public Golem(int row, int column, int wayOut) {
        this.row = row;
        this.column = column;
        this.wayOut = wayOut;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정답 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음 삼성 기출 풀때 느끼는 거지만 정답 코드처럼은 절대 못 짤듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 깔끔하네요..&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1744301235444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class Main {
    private static final int MAX_L = 70;

    private static int R, C, K; // 행, 열, 골렘의 개수를 의미합니다
    private static int[][] A = new int[MAX_L + 3][MAX_L]; // 실제 숲을 [3~R+2][0~C-1]로 사용하기위해 행은 3만큼의 크기를 더 갖습니다
    private static int[] dy = {-1, 0, 1, 0}, dx = {0, 1, 0, -1};
    private static boolean[][] isExit = new boolean[MAX_L + 3][MAX_L]; // 해당 칸이 골렘의 출구인지 저장합니다
    private static int answer = 0; // 각 정령들이 도달할 수 있는 최하단 행의 총합을 저장합니다

    // (y, x)가 숲의 범위 안에 있는지 확인하는 함수입니다
    private static boolean inRange(int y, int x) {
        return 3 &amp;lt;= y &amp;amp;&amp;amp; y &amp;lt; R + 3 &amp;amp;&amp;amp; 0 &amp;lt;= x &amp;amp;&amp;amp; x &amp;lt; C;
    }

    // 숲에 있는 골렘들이 모두 빠져나갑니다
    private static void resetMap() {
        for (int i = 0; i &amp;lt; R + 3; i++) {
            for (int j = 0; j &amp;lt; C; j++) {
                A[i][j] = 0;
                isExit[i][j] = false;
            }
        }
    }

    // 골렘의 중심이 y, x에 위치할 수 있는지 확인합니다.
    // 북쪽에서 남쪽으로 내려와야하므로 중심이 (y, x)에 위치할때의 범위와 (y-1, x)에 위치할떄의 범위 모두 확인합니다
    private static boolean canGo(int y, int x) {
        boolean flag = 0 &amp;lt;= x - 1 &amp;amp;&amp;amp; x + 1 &amp;lt; C &amp;amp;&amp;amp; y + 1 &amp;lt; R + 3;
        flag = flag &amp;amp;&amp;amp; (A[y - 1][x - 1] == 0);
        flag = flag &amp;amp;&amp;amp; (A[y - 1][x] == 0);
        flag = flag &amp;amp;&amp;amp; (A[y - 1][x + 1] == 0);
        flag = flag &amp;amp;&amp;amp; (A[y][x - 1] == 0);
        flag = flag &amp;amp;&amp;amp; (A[y][x] == 0);
        flag = flag &amp;amp;&amp;amp; (A[y][x + 1] == 0);
        flag = flag &amp;amp;&amp;amp; (A[y + 1][x] == 0);
        return flag;
    }

    // 정령이 움직일 수 있는 모든 범위를 확인하고 도달할 수 있는 최하단 행을 반환합니다
    private static int bfs(int y, int x) {
        int result = y;
        Queue&amp;lt;int[]&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
        boolean[][] visit = new boolean[MAX_L + 3][MAX_L];
        q.offer(new int[]{y, x});
        visit[y][x] = true;
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            for (int k = 0; k &amp;lt; 4; k++) {
                int ny = cur[0] + dy[k], nx = cur[1] + dx[k];
                // 정령의 움직임은 골렘 내부이거나
                // 골렘의 탈출구에 위치하고 있다면 다른 골렘으로 옮겨 갈 수 있습니다
                if (inRange(ny, nx) &amp;amp;&amp;amp; !visit[ny][nx] &amp;amp;&amp;amp; (A[ny][nx] == A[cur[0]][cur[1]] || (A[ny][nx] != 0 &amp;amp;&amp;amp; isExit[cur[0]][cur[1]]))) {
                    q.offer(new int[]{ny, nx});
                    visit[ny][nx] = true;
                    result = Math.max(result, ny);
                }
            }
        }
        return result;
    }

    // 골렘id가 중심 (y, x), 출구의 방향이 d일때 규칙에 따라 움직임을 취하는 함수입니다
    // 1. 남쪽으로 한 칸 내려갑니다.
    // 2. (1)의 방법으로 이동할 수 없으면 서쪽 방향으로 회전하면서 내려갑니다.
    // 3. (1)과 (2)의 방법으로 이동할 수 없으면 동쪽 방향으로 회전하면서 내려갑니다.
    private static void down(int y, int x, int d, int id) {
        if (canGo(y + 1, x)) {
            // 아래로 내려갈 수 있는 경우입니다
            down(y + 1, x, d, id);
        } else if (canGo(y + 1, x - 1)) {
            // 왼쪽 아래로 내려갈 수 있는 경우입니다
            down(y + 1, x - 1, (d + 3) % 4, id);
        } else if (canGo(y + 1, x + 1)) {
            // 오른쪽 아래로 내려갈 수 있는 경우입니다
            down(y + 1, x + 1, (d + 1) % 4, id);
        } else {
            // 1, 2, 3의 움직임을 모두 취할 수 없을떄 입니다.
            if (!inRange(y-1, x-1) || !inRange(y+1, x+1)) {
                // 숲을 벗어나는 경우 모든 골렘이 숲을 빠져나갑니다
                resetMap();
            } else {
                // 골렘이 숲 안에 정착합니다
                A[y][x] = id;
                for (int k = 0; k &amp;lt; 4; k++)
                    A[y + dy[k]][x + dx[k]] = id;
                // 골렘의 출구를 기록하고
                isExit[y + dy[d]][x + dx[d]] = true;
                // bfs를 통해 정령이 최대로 내려갈 수 있는 행를 계산하여 누적합합니다
                answer += bfs(y, x) - 3 + 1;
            }
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        R = scanner.nextInt();
        C = scanner.nextInt();
        K = scanner.nextInt();
        for (int id = 1; id &amp;lt;= K; id++) { // 골렘 번호 id
            int x = scanner.nextInt() - 1;
            int d = scanner.nextInt();
            down(0, x, d, id);
        }
        System.out.println(answer);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>PS</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/93</guid>
      <comments>https://oxdjww.tistory.com/93#entry93comment</comments>
      <pubDate>Fri, 11 Apr 2025 01:10:44 +0900</pubDate>
    </item>
    <item>
      <title>[Codetree] 왕실의 기사 대결</title>
      <link>https://oxdjww.tistory.com/92</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://www.codetree.ai/ko/frequent-problems/problems/royal-knight-duel/description?introductionSetId=&amp;amp;bookmarkId=&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #46474c; text-align: start;&quot;&gt;2023 하반기 오전 1번 문제&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 설계 1시간 걸리고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 2시간 하고, 디버깅 1시간 하다가 로직이 답이 없음을 깨닫고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답 코드를 이해하고 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 시뮬레이션으로 구현하려다가, 풀이는 bfs던데 하나의 조각 단위로 밀면서 계산하는 방식이 너무 어려워서..&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;기사 다음 칸 이동 가능한지 검사(DFS)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기사 다음칸 이동(DFS)&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중 DFS로 구현했다.&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시간, 예외처리 + 테케 맞추는 데에 15분? 정도 걸렸다.&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;체스판(board)에는 빈칸/함정/벽 기록하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기사판(knightBoard)에는 기사들의 위치를 기록했다&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;그리고 Map&amp;lt;int[]&amp;gt;로 각 기사들의 분포 위치를 (r,c)와 (h,w)에 맞게 넣어주고 contains로 비교하려 했으나..&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;그냥 Knight[] 배열에 참조 테이블을 두어서 그때그때 기록하고, 변경점이 있으면 매번 순회하는 게 디버깅하기 쉬울 것 같아서 그렇게 했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 복잡도가 높아도 삼성 문제는 &lt;b&gt;구현력&lt;/b&gt;을 더 중시하는 경향인 것 같아서.. 사실 시간 초과 걱정을 안 해도 돼서 되게 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 구현하고, DFS 무한 재귀 stack overflow가 두어번 발생했지만 오타나 i,j를 잘못 본 것이라 생각외로 구현이 빠르게 되었다.&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;a href=&quot;https://oxdjww.tistory.com/91&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;포탑 부수기&quot;)&lt;/a&gt; 에서도 전체 테케에 대한 예외처리가 필수였다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. DFS 조건 잘 짜기&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;4. 자료구조 선택 (Knight[], board를 두개 둔 것 등)&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;번외로, 여태 PS 풀면서 재밌다 느낀 적 별로 없었는데,, 삼성 코테는 스트레스도 받지만 생각보다 재밌네요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제출 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1744271618970&quot; class=&quot;arduino&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.util.*;
import java.io.*;

public class Main {
    private static int L;
    private static int N;
    private static int Q;
    private static int[][] board, knightBoard;
    private static Knight[] knightStatus;
    private static int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    private static boolean[] moved;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        L = Integer.parseInt(st.nextToken());
        N = Integer.parseInt(st.nextToken());
        Q = Integer.parseInt(st.nextToken());

        board = new int[L][L];
        knightBoard = new int[L][L];
        knightStatus = new Knight[N+1];
        moved = new boolean[N+1];
        // 체스판 초기화
        for(int i = 0; i &amp;lt; L; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j &amp;lt; L; j++) {
                board[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        // 초기 기사 정보
        for(int knightNumber = 1; knightNumber &amp;lt;= N; knightNumber++) {
            st = new StringTokenizer(br.readLine());
            int r = Integer.parseInt(st.nextToken());
            int c = Integer.parseInt(st.nextToken());
            int h = Integer.parseInt(st.nextToken());
            int w = Integer.parseInt(st.nextToken());
            int k = Integer.parseInt(st.nextToken());

            putKnightsOnTheBoard(knightNumber, r - 1, c - 1, h, w, k);
        }
        // print(board);
        // print(knightBoard);
        // printKnightStatus();

        for(int i = 0; i &amp;lt; Q; i++) {
            st = new StringTokenizer(br.readLine());
            int commandKnight = Integer.parseInt(st.nextToken());
            int direction = Integer.parseInt(st.nextToken());

            if(knightStatus[commandKnight].alive){
                execute(commandKnight, direction);
            }
        }

        int answer = 0;
        for(int i = 1; i &amp;lt; knightStatus.length; i++) {
            Knight node = knightStatus[i];
            // System.out.println(i + &quot;번 기사(&quot; + node.alive + &quot;) 누적 데미지: &quot; + node.damage);
            if(node.alive) {
                answer += node.damage;
            }
        }
        System.out.println(answer);
    }

    private static void printKnightStatus() {
        for(int i = 1 ; i &amp;lt; knightStatus.length ; i++) {
            System.out.println(knightStatus[i].row);
            System.out.println(knightStatus[i].column);
            System.out.println(knightStatus[i].height);
            System.out.println(knightStatus[i].width);
            System.out.println(knightStatus[i].power);
        }
    }

    private static void putKnightsOnTheBoard(int knightNumber, int r, int c, int h, int w, int k) {
        for(int i = r; i &amp;lt; r + h; i++) {
            for(int j = c; j &amp;lt; c + w; j++) {
                knightBoard[i][j] = knightNumber;
            }
        }
        knightStatus[knightNumber] = new Knight(r, c, h, w, k);
    }

    private static void print(int[][] arr) {
        for(int i = 0 ; i &amp;lt; L ; i++) {
            for(int j = 0 ; j &amp;lt; L ; j++) {
                System.out.print(arr[i][j] + &quot; &quot;);
            }
            System.out.println();
        }
    }

    private static void execute(int commandKnight, int direction) {
        // System.out.println(commandKnight + &quot; MOVE TO &quot; + direction);
        // 움직일 수 없으면 continue;
        if(!canMove(commandKnight, direction)) {
            // System.out.println(commandKnight + &quot;CAN'T MOVE&quot;);
            return;
        }
        // System.out.println(&quot;CAN MOVE!!&quot;);
        // 이동
        moveKnight(commandKnight, direction);
        // print(knightBoard);
        // 데미지 계산(commandKnight 제외)
        calculateDamage(commandKnight);
        // print(knightBoard);
        Arrays.fill(moved, false);
    }

    private static void calculateDamage(int excludeKnight) {
        for (int i = 1; i &amp;lt;= N; i++) {
            if (!moved[i] || i == excludeKnight || !knightStatus[i].alive) continue;

            int damage = 0;
            Knight knight = knightStatus[i];

            for (int r = knight.row; r &amp;lt; knight.row + knight.height; r++) {
                for (int c = knight.column; c &amp;lt; knight.column + knight.width; c++) {
                    if (board[r][c] == 1) {
                        damage++;
                    }
                }
            }

            knight.power -= damage;
            knight.damage += damage;

            if (knight.power &amp;lt;= 0) {
                killKnight(i);
            }
        }
    }


    private static void killKnight(int knightNumber) {
        knightStatus[knightNumber].alive = false;
        for(int i = 0; i &amp;lt; L; i++) {
            for(int j = 0; j &amp;lt; L; j++) {
                if(knightBoard[i][j] == knightNumber) {
                    knightBoard[i][j] = 0;
                }
            }
        }
    }

    private static boolean canMove(int knightNumber, int direction) {
        int row = knightStatus[knightNumber].row;
        int column = knightStatus[knightNumber].column;
        int height = knightStatus[knightNumber].height;
        int width = knightStatus[knightNumber].width;
        // System.out.println(&quot;TRY &quot; + knightNumber + &quot;(&quot; + row + &quot;, &quot; + column + &quot;) MOVE TO &quot; + direction);
        
        for(int i = row; i &amp;lt; row + height; i++) {
            for(int j = column; j &amp;lt; column + width; j++) {
                int nr = i + directions[direction][0];
                int nc = j + directions[direction][1];

                // 벽 체크
                if(isWall(nr,nc)) {
                    return false;
                }

                int next = knightBoard[nr][nc];

                if(next == 0 || next == (knightNumber)) {
                    continue;
                }

                if(!canMove(next, direction)) {
                    return false;
                }
            }
        }
        return true;
    }

    static boolean isWall(int r, int c) {
        return r &amp;lt; 0 || c &amp;lt; 0 || r &amp;gt;= L || c &amp;gt;= L || board[r][c] == 2;
    }
    
    private static void moveKnight(int knightNumber, int direction) {
        int row = knightStatus[knightNumber].row;
        int column = knightStatus[knightNumber].column;
        int height = knightStatus[knightNumber].height;
        int width = knightStatus[knightNumber].width;
        // System.out.println(&quot;MOVE &quot; + knightNumber + &quot;(&quot; + row + &quot;, &quot; + column + &quot;) MOVE TO &quot; + direction);

        for(int i = row; i &amp;lt; row + height; i++) {
            for(int j = column; j &amp;lt; column + width; j++) {
                int nr = i + directions[direction][0];
                int nc = j + directions[direction][1];

                int next = knightBoard[nr][nc];

                if(next == 0 || next == knightNumber) {
                    continue;
                }
                
                moveKnight(next, direction);
            }
        }

        moved[knightNumber] = true;

        switch(direction) {
            case 0:
                moveUp(knightNumber, direction);
                break;
            case 1:
                moveRight(knightNumber, direction);
                break;
            case 2:
                moveDown(knightNumber, direction);
                break;
            case 3:
                moveLeft(knightNumber, direction);
                break;
        }

        knightStatus[knightNumber].row += directions[direction][0];
        knightStatus[knightNumber].column += directions[direction][1];
    }

    private static void moveUp(int knightNumber, int direction) {
        int row = knightStatus[knightNumber].row;
        int column = knightStatus[knightNumber].column;
        int height = knightStatus[knightNumber].height;
        int width = knightStatus[knightNumber].width;

        for (int r = row; r &amp;lt; row + height; r++) {
            for (int c = column; c &amp;lt; column + width; c++) {
                int nr =  r + directions[direction][0];
                int nc =  c + directions[direction][1];

                knightBoard[r][c] = 0;
                knightBoard[nr][nc] = (knightNumber);
            }
        }
    }

    private static void moveRight(int knightNumber, int direction) {
        int row = knightStatus[knightNumber].row;
        int column = knightStatus[knightNumber].column;
        int height = knightStatus[knightNumber].height;
        int width = knightStatus[knightNumber].width;

        for (int c = column + width - 1; c &amp;gt;= column; c--) {
            for (int r = row; r &amp;lt; row + height; r++) {
                int nr =  r + directions[direction][0];
                int nc =  c + directions[direction][1];

                knightBoard[r][c] = 0;
                knightBoard[nr][nc] = (knightNumber);
            }
        }
    }

    private static void moveDown(int knightNumber, int direction) {
        int row = knightStatus[knightNumber].row;
        int column = knightStatus[knightNumber].column;
        int height = knightStatus[knightNumber].height;
        int width = knightStatus[knightNumber].width;

        for (int r = row + height - 1; r &amp;gt;= row; r--) {
            for (int c = column; c &amp;lt; column + width; c++) {
                int nr =  r + directions[direction][0];
                int nc =  c + directions[direction][1];

                knightBoard[r][c] = 0;
                knightBoard[nr][nc] = (knightNumber);
            }
        }
    }

    private static void moveLeft(int knightNumber, int direction) {
        int row = knightStatus[knightNumber].row;
        int column = knightStatus[knightNumber].column;
        int height = knightStatus[knightNumber].height;
        int width = knightStatus[knightNumber].width;

        for (int c = column; c &amp;lt; column + width; c++) {
            for (int r = row; r &amp;lt; row + height; r++) {
                int nr =  r + directions[direction][0];
                int nc =  c + directions[direction][1];

                knightBoard[r][c] = 0;
                knightBoard[nr][nc] = (knightNumber);
            }
        }
    }
}

class Knight {
    int row;
    int column;
    int height;
    int width;
    int power;
    int damage = 0;
    boolean alive = true;

    public Knight(int row, int column, int height, int width, int power) {
        this.row = row;
        this.column = column;
        this.height = height;
        this.width = width;
        this.power = power;
        this.alive = true;
    }
}&lt;/code&gt;&lt;/pre&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;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1744271646804&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.util.*;

public class Main {
    public static final int MAX_N = 31;
    public static final int MAX_L = 41;

    public static int l, n, q;
    public static int[][] info = new int[MAX_L][MAX_L];
    public static int[] bef_k = new int[MAX_N];
    public static int[] r = new int[MAX_N], c = new int[MAX_N], h = new int[MAX_N], w = new int[MAX_N], k = new int[MAX_N];
    public static int[] nr = new int[MAX_N], nc = new int[MAX_N];
    public static int[] dmg = new int[MAX_N];
    public static boolean[] is_moved = new boolean[MAX_N];

    public static int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};

    // 움직임을 시도해봅니다.
    public static boolean tryMovement(int idx, int dir) {
        Queue&amp;lt;Integer&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
        boolean is_pos = true;

        // 초기화 작업입니다.
        for(int i = 1; i &amp;lt;= n; i++) {
            dmg[i] = 0;
            is_moved[i] = false;
            nr[i] = r[i];
            nc[i] = c[i];
        }

        q.add(idx);
        is_moved[idx] = true;

        while(!q.isEmpty()) {
            int x = q.poll();

            nr[x] += dx[dir];
            nc[x] += dy[dir];

            // 경계를 벗어나는지 체크합니다.
            if(nr[x] &amp;lt; 1 || nc[x] &amp;lt; 1 || nr[x] + h[x] - 1 &amp;gt; l || nc[x] + w[x] - 1 &amp;gt; l)
                return false;

            // 대상 조각이 다른 조각이나 장애물과 충돌하는지 검사합니다.
            for(int i = nr[x]; i &amp;lt;= nr[x] + h[x] - 1; i++) {
                for(int j = nc[x]; j &amp;lt;= nc[x] + w[x] - 1; j++) {
                    if(info[i][j] == 1) 
                        dmg[x]++;
                    if(info[i][j] == 2)
                        return false;
                }
            }

            // 다른 조각과 충돌하는 경우, 해당 조각도 같이 이동합니다.
            for(int i = 1; i &amp;lt;= n; i++) {
                if(is_moved[i] || k[i] &amp;lt;= 0) 
                    continue;
                if(r[i] &amp;gt; nr[x] + h[x] - 1 || nr[x] &amp;gt; r[i] + h[i] - 1) 
                    continue;
                if(c[i] &amp;gt; nc[x] + w[x] - 1 || nc[x] &amp;gt; c[i] + w[i] - 1) 
                    continue;

                is_moved[i] = true;
                q.add(i);
            }
        }

        dmg[idx] = 0;
        return true;
    }

    // 특정 조각을 지정된 방향으로 이동시키는 함수입니다.
    public static void movePiece(int idx, int dir) {
        if(k[idx] &amp;lt;= 0) return;

        // 이동이 가능한 경우, 실제 위치와 체력을 업데이트합니다.
        if(tryMovement(idx, dir)) {
            for(int i = 1; i &amp;lt;= n; i++) {
                r[i] = nr[i];
                c[i] = nc[i];
                k[i] -= dmg[i];
            }
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        // 입력값을 받습니다.
        l = sc.nextInt();
        n = sc.nextInt();
        q = sc.nextInt();

        for(int i = 1; i &amp;lt;= l; i++)
            for(int j = 1; j &amp;lt;= l; j++)
                info[i][j] = sc.nextInt();

        for(int i = 1; i &amp;lt;= n; i++) {
            r[i] = sc.nextInt();
            c[i] = sc.nextInt();
            h[i] = sc.nextInt();
            w[i] = sc.nextInt();
            k[i] = sc.nextInt();
            bef_k[i] = k[i];
        }

        for(int i = 1; i &amp;lt;= q; i++) {
            int idx = sc.nextInt();
            int dir = sc.nextInt();
            movePiece(idx, dir);
        }

        // 결과를 계산하고 출력합니다.
        long ans = 0;
        for(int i = 1; i &amp;lt;= n; i++) {
            if(k[i] &amp;gt; 0) {
                ans += bef_k[i] - k[i];
            }
        }

        System.out.println(ans);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>PS</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/92</guid>
      <comments>https://oxdjww.tistory.com/92#entry92comment</comments>
      <pubDate>Thu, 10 Apr 2025 16:59:30 +0900</pubDate>
    </item>
    <item>
      <title>[Codetree] 포탑 부수기</title>
      <link>https://oxdjww.tistory.com/91</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://www.codetree.ai/ko/frequent-problems/problems/destroy-the-turret&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #46474c; text-align: start;&quot;&gt;2023 상반기 오전 1번 문제&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수 시간 5시간 정도 갈아 넣은 끝에 구현한 포탑 부수기..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 문제 이해하고 설계를 했으나 객체지향 특징은 잘 못 살리고 절차지향으로 설계해서 디버깅이 오래 걸렸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 구현은 2시간 안으로 됐는데, 제공하는 테케 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;2% 4% 6% 8%에서 계속 끊겼었음..&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;제출 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1744188319746&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {
    private static Tower[][] board;
    private static int n;
    private static int m;
    private static int k;
    private static boolean[][] previousAttack;
    private static boolean[][] visited;

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        StringTokenizer st = new StringTokenizer(br.readLine());

        n = Integer.parseInt(st.nextToken());
        m = Integer.parseInt(st.nextToken());
        k = Integer.parseInt(st.nextToken());

        board = new Tower[n][m];
        previousAttack = new boolean[n][m];
        visited = new boolean[n][m];
        for(int i = 0; i &amp;lt; n; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j &amp;lt; m; j++) {
                board[i][j] = new Tower(
                    i, j,
                    Integer.parseInt(st.nextToken())
                );
            }
        }        

        for(int i = 1; i &amp;lt;= k; i++) {
            int[] attacker = selectAttacker();
            if (attacker == null) break;
            // 공격자도 공격 관련에 포함
            previousAttack[attacker[0]][attacker[1]] = true;
            // 최근 공격 저장
            board[attacker[0]][attacker[1]].attackHistory = i;
            // 버프
            board[attacker[0]][attacker[1]].power += (n+m);
            // System.out.println(attacker[0] + &quot;, &quot; + attacker[1]);
            int[] victim = selectVictim(attacker);
            if (victim == null) break;

            // System.out.println(victim[0] + &quot;, &quot; + victim[1]);
            attack(attacker, victim);
            repair();
            if (countAliveTowers() &amp;lt;= 1) break;
        }
        int max = Integer.MIN_VALUE;
        for(int i = 0; i &amp;lt; board.length; i++) {
            for(int j = 0; j &amp;lt; board[i].length; j++) {
                max = Math.max(max, board[i][j].power);
            }
        }
        System.out.println(max);
    }

    private static int countAliveTowers() {
        int cnt = 0;
        for (int i = 0; i &amp;lt; n; i++) {
            for (int j = 0; j &amp;lt; m; j++) {
                if (board[i][j].power &amp;gt; 0) cnt++;
            }
        }
        return cnt;
    }


    private static int[] selectAttacker() {
        int minPower = Integer.MAX_VALUE;
        
        for(int i = 0; i &amp;lt; n; i++) {
            for(int j = 0; j &amp;lt; m; j++) {
                if(board[i][j].power != 0) {
                    minPower = Math.min(minPower, board[i][j].power);
                }
            }
        }

        if(minPower == Integer.MAX_VALUE) return null;

        List&amp;lt;Tower&amp;gt; smallerPower = new ArrayList&amp;lt;&amp;gt;();

        for(int i = 0; i &amp;lt; n; i++) {
            for(int j = 0; j &amp;lt; m; j++) {
                if(board[i][j].power != 0) {
                    if(board[i][j].power == minPower) {
                        smallerPower.add(board[i][j]);
                    }
                }                                       
            }
        }

        if(smallerPower.size() == 1) {
            // 1. 공격력 가장 낮은 포탑 (1개)
            return new int[]{smallerPower.get(0).x, smallerPower.get(0).y};
        } else {
            // 2. 가장 최근 공격
            Collections.sort(smallerPower, new Comparator&amp;lt;Tower&amp;gt;() {
                @Override
                public int compare(Tower t1, Tower t2) {
                    return Integer.compare(t2.attackHistory, t1.attackHistory);
                }
            });
            int recentMin = smallerPower.get(0).attackHistory;
            List&amp;lt;Tower&amp;gt; recent = new ArrayList&amp;lt;&amp;gt;();
            for(int i = 0; i &amp;lt; smallerPower.size(); i++) {
                if(smallerPower.get(i).attackHistory == recentMin) {
                    recent.add(smallerPower.get(i));
                }
            }

            if(recent.size() == 1) {
                return new int[]{recent.get(0).x, recent.get(0).y};
            } else {
                // 3. 행 열 합 가장 큰
                Collections.sort(recent, new Comparator&amp;lt;Tower&amp;gt;() {
                    @Override
                    public int compare(Tower t1, Tower t2) {
                        return Integer.compare(t2.x + t2.y, t1.x + t1.y);
                    }
                });
                int rowColSumMax = recent.get(0).x + recent.get(0).y;
                List&amp;lt;Tower&amp;gt; rowColSum = new ArrayList&amp;lt;&amp;gt;();
                for(int i = 0; i &amp;lt; recent.size(); i++) {
                    if(recent.get(i).x + recent.get(i).y == rowColSumMax) {
                        rowColSum.add(recent.get(i));
                    }
                }
                                    
                if(rowColSum.size() == 1) {
                    return new int[]{rowColSum.get(0).x, rowColSum.get(0).y};
                } else {
                    // 4. 열 가장 큰
                    Collections.sort(rowColSum, new Comparator&amp;lt;Tower&amp;gt;() {
                        @Override
                        public int compare(Tower t1, Tower t2) {
                            return Integer.compare(t2.y, t1.y);
                        }
                    });
                    return new int[]{rowColSum.get(0).x, rowColSum.get(0).y};
                }
            }
        }
    } // selectAttacker()

    private static int[] selectVictim(int[] attacker) {
        int maxPower = Integer.MIN_VALUE;
        
        for(int i = 0; i &amp;lt; n; i++) {
            for(int j = 0; j &amp;lt; m; j++) {
                if(board[i][j].power != 0 &amp;amp;&amp;amp; !(i == attacker[0] &amp;amp;&amp;amp; j == attacker[1])) {
                    maxPower = Math.max(maxPower, board[i][j].power);
                }
            }
        }
        
        if(maxPower == Integer.MIN_VALUE) return null;

        List&amp;lt;Tower&amp;gt; biggestPower = new ArrayList&amp;lt;&amp;gt;();

        for(int i = 0; i &amp;lt; n; i++) {
            for(int j = 0; j &amp;lt; m; j++) {
                if(board[i][j].power != 0) {
                    if(board[i][j].power == maxPower &amp;amp;&amp;amp; !(i == attacker[0] &amp;amp;&amp;amp; j == attacker[1])) {
                        biggestPower.add(board[i][j]);
                    }
                }                                       
            }
        }

        if(biggestPower.size() == 1) {
            // 1. 공격력 가장 높은 포탑 (1개)
            return new int[]{biggestPower.get(0).x, biggestPower.get(0).y};
        } else {
            // 2. 가장 오래된 공격
            Collections.sort(biggestPower, new Comparator&amp;lt;Tower&amp;gt;() {
                @Override
                public int compare(Tower t1, Tower t2) {
                    return Integer.compare(t1.attackHistory, t2.attackHistory);
                }
            });
            int recentMin = biggestPower.get(0).attackHistory;
            List&amp;lt;Tower&amp;gt; recent = new ArrayList&amp;lt;&amp;gt;();
            for(int i = 0; i &amp;lt; biggestPower.size(); i++) {
                if(biggestPower.get(i).attackHistory == recentMin) {
                    recent.add(biggestPower.get(i));
                }
            }

            if(recent.size() == 1) {
                return new int[]{recent.get(0).x, recent.get(0).y};
            } else {
                // 3. 행 열 합 가장 작은
                Collections.sort(recent, new Comparator&amp;lt;Tower&amp;gt;() {
                    @Override
                    public int compare(Tower t1, Tower t2) {
                        return Integer.compare(t1.x + t1.y, t2.x + t2.y);
                    }
                });
                int rowColSumMax = recent.get(0).x + recent.get(0).y;
                List&amp;lt;Tower&amp;gt; rowColSum = new ArrayList&amp;lt;&amp;gt;();
                for(int i = 0; i &amp;lt; recent.size(); i++) {
                    if(recent.get(i).x + recent.get(i).y == rowColSumMax) {
                        rowColSum.add(recent.get(i));
                    }
                }
                                    
                if(rowColSum.size() == 1) {
                    return new int[]{rowColSum.get(0).x, rowColSum.get(0).y};
                } else {
                    // 4. 열 가장 작은
                    Collections.sort(rowColSum, new Comparator&amp;lt;Tower&amp;gt;() {
                        @Override
                        public int compare(Tower t1, Tower t2) {
                            return Integer.compare(t1.y, t2.y);
                        }
                    });
                    return new int[]{rowColSum.get(0).x, rowColSum.get(0).y};
                }
            }
        }
    } // selectVictim()

    private static void attack(int[] attacker, int[] target) {
        visited = new boolean[n][m];
        Queue&amp;lt;Point&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
        List&amp;lt;int[]&amp;gt; tmp = new ArrayList&amp;lt;&amp;gt;();
        tmp.add(new int[]{attacker[0], attacker[1]});
        Point start = new Point(attacker[0], attacker[1], tmp);
        queue.offer(start);
        visited[attacker[0]][attacker[1]] = true;
        Point shortestPathPoint = null;
        while(!queue.isEmpty()) {
            Point current = queue.poll();
            int cx = current.x;
            int cy = current.y;
            // System.out.println(&quot;POLL &quot; + current + &quot;: &quot; + cx + &quot;, &quot; + cy);
            if(cx == target[0] &amp;amp;&amp;amp; cy == target[1]) {
                // 최단거리 탐색 완료
                shortestPathPoint = current;
                break;
            }
            int[] dx = {0, 1, 0, -1};
            int[] dy = {1, 0, -1, 0};

            for(int i = 0; i &amp;lt; 4; i++) {
                int nx = cx + dx[i];
                int ny = cy + dy[i];
                // 경계 벗어난다면 반대편으로
                if(nx &amp;lt; 0) nx = n-1;
                if(nx &amp;gt;= n) nx = 0;
                if(ny &amp;lt; 0) ny = m-1;
                if(ny &amp;gt;= m) ny = 0;
                if(board[nx][ny].power != 0 &amp;amp;&amp;amp; !visited[nx][ny]) {
                    List&amp;lt;int[]&amp;gt; newPath = new ArrayList&amp;lt;&amp;gt;(current.path);
                    newPath.add(new int[]{nx, ny});
                    queue.offer(new Point(nx, ny, newPath));
                    visited[nx][ny] = true;

                    // System.out.println(&quot;OFFER &quot; + input + &quot;: &quot; + nx + &quot;, &quot; + ny);
                    visited[nx][ny] = true;
                }
            }
        } // while
        // System.out.println(&quot;SHORTEST PATH&quot;);
        // if(shortestPathPoint != null ) {
        //         for(int i = 0 ; i &amp;lt; shortestPathPoint.path.size() ; i++) {
        //         System.out.print(shortestPathPoint.path.get(i)[0] + &quot;, &quot; + shortestPathPoint.path.get(i)[1]);
        //         System.out.println();
        //     }
        // }
        // System.out.println();
        if(shortestPathPoint == null) {
            // 포탄 공격
            // print();
            // System.out.println(&quot;POTAN&quot;);
            potanAttack(attacker, target);
            // print();
        } else {
            // 레이저 공격 로직
            // print();
            // System.out.println(&quot;LAZER&quot;);
            lazerAttack(attacker, shortestPathPoint);
            // print();
        }
    } // attack(int[], int[])
    private static void lazerAttack(int[] attacker, Point target) {
        // System.out.println(&quot;LAZER ATTACK&quot;);
        List&amp;lt;int[]&amp;gt; path = target.path;
        int attackerPower = board[attacker[0]][attacker[1]].power;

        for(int i = 0; i &amp;lt; path.size(); i++) {
            int x = path.get(i)[0];
            int y = path.get(i)[1];
            // System.out.println(&quot;LAZER :&quot; + x + &quot;, &quot; + y);
            if(i == path.size() - 1) {
                // 공격지점
                // System.out.println(board[x][y].power + &quot; -= &quot; + attackerPower);
                int result = board[x][y].power - attackerPower;
                board[x][y].power = (result &amp;lt; 0 ? 0 : result);
            } else if(i != 0) {
                // 경로
                // System.out.println(board[x][y].power + &quot; -= &quot; + attackerPower/2);
                int result = board[x][y].power - attackerPower/2;
                board[x][y].power = (result &amp;lt; 0 ? 0 : result);
            }
            previousAttack[x][y] = true;
        }
    }
    private static void potanAttack(int[] attacker, int[] target) {
        // System.out.println(&quot;POTAN ATTACK&quot;);
        int attackPower = board[attacker[0]][attacker[1]].power;
        int targetX = target[0];
        int targetY = target[1];
        int targetAttackResult = board[targetX][targetY].power - attackPower;
        board[targetX][targetY].power = targetAttackResult &amp;lt; 0 ? 0 : targetAttackResult;
        previousAttack[targetX][targetY] = true;
        int[] dx = {-1, -1, -1, 0, 0, 1, 1, 1};
        int[] dy = {-1, 0, 1, -1, 1, -1, 0, 1};
        for(int i = 0; i &amp;lt; dx.length; i++) {
            int nx = targetX + dx[i];
            int ny = targetY + dy[i];

            // 경계 벗어난다면 반대편으로
            if(nx &amp;lt; 0) nx = n - 1;
            if(nx &amp;gt;= n) nx = 0;
            if(ny &amp;lt; 0) ny = m - 1;
            if(ny &amp;gt;= m) ny = 0;

            // 자신은 제외
            if(nx == attacker[0] &amp;amp;&amp;amp; ny == attacker[1]) continue;

            int result = board[nx][ny].power - attackPower/2;
            board[nx][ny].power = result &amp;lt; 0 ? 0 : result;
            previousAttack[nx][ny] = true;
        }
    }
    private static void print() {
        for(int i = 0 ; i &amp;lt; board.length ; i++) {
            for(int j = 0 ; j &amp;lt; board[i].length ; j++) {
                System.out.print(board[i][j].power + &quot; &quot;);
            }
            System.out.println();
        }
    }
    private static void repair() {
        // System.out.println(&quot;REPAIR&quot;);
        for(int i = 0 ; i &amp;lt; board.length ; i++) {
            for(int j = 0 ; j &amp;lt; board[i].length ; j++) {
                if(!previousAttack[i][j]) {
                    if(board[i][j].power != 0) {
                        board[i][j].power += 1;
                    }
                }
            }
        }
        for (int i = 0; i &amp;lt; n; i++) {
            Arrays.fill(previousAttack[i], false);
        }
    }
}

class Tower {
    int x;
    int y;
    int power;
    int attackHistory;
    
    public Tower(int x, int y, int power) {
        this.x = x;
        this.y = y;
        this.power = power;
        this.attackHistory = 0;
    }
}
class Point {
        int x;
        int y;
        List&amp;lt;int[]&amp;gt; path;
        public Point(int x, int y, List&amp;lt;int[]&amp;gt; path) {
            this.x = x;
            this.y = y;
            this.path = path;
        }
    }&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Codetree 제공 모범답안 코드&lt;/h4&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1744188721752&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Scanner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Queue;
import java.util.LinkedList;

class Pair {
    int x, y;

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

// class turret을 정의해 관리합니다.
class Turret implements Comparable&amp;lt;Turret&amp;gt; {
    int x, y, r, p;

    public Turret(int x, int y, int r, int p) {
        this.x = x;
        this.y = y;
        this.r = r;
        this.p = p;
    }

    // turret의 약함, 강함 우선순위에 맞게 정렬함수를 만들어줍니다.
    public int compareTo(Turret t) {
        if(this.p != t.p) return this.p - t.p;
        if(this.r != t.r) return t.r - this.r;
        if(this.x + this.y != t.x + t.y) return (t.x + t.y) - (this.x + this.y);
        return t.y - this.y;
    }
};

public class Main {
    public static final int MAX_N = 10;
    
    public static int[] dx = new int[]{0, 1, 0, -1};
    public static int[] dy = new int[]{1, 0, -1, 0};
    public static int[] dx2 = new int[]{0, 0, 0, -1, -1, -1, 1, 1, 1};
    public static int[] dy2 = new int[]{0, -1, 1, 0, -1, 1, 0, -1, 1};
    
    public static int n, m, k;
    public static int turn;
    
    // 현재 포탑들이 가진 힘과 언제 각성했는지 기록해줍니다.
    public static int[][] board = new int[MAX_N][MAX_N];
    public static int[][] rec = new int[MAX_N][MAX_N];
    
    // 빛의 공격을 할 때 방문 여부와 경로 방향을 기록해줍니다.
    public static boolean[][] vis = new boolean[MAX_N][MAX_N];
    public static int[][] backX = new int[MAX_N][MAX_N];
    public static int[][] backY = new int[MAX_N][MAX_N];
    
    // 공격과 무관했는지 여부를 저장합니다.
    public static boolean[][] isActive = new boolean[MAX_N][MAX_N];
    
    // 살아있는 포탑들을 관리합니다.
    public static ArrayList&amp;lt;Turret&amp;gt; liveTurret = new ArrayList&amp;lt;&amp;gt;();
    
    // 턴을 진행하기 전 필요한 전처리를 정리해줍니다.
    public static void init() {
        turn++;
        for(int i = 0; i &amp;lt; n; i++)
            for(int j = 0; j &amp;lt; m; j++) {
                vis[i][j] = false;
                isActive[i][j] = false;
            }
    }
    
    // 각성을 진행합니다.
    // 각성을 하면 가장 약한 포탑이 n + m만큼 강해집니다.
    public static void awake() {
        // 우선순위에 맞게 현재 살아있는 포탑들을 정렬해줍니다.
        Collections.sort(liveTurret);
    
        // 가장 약한 포탑을 찾아 n + m만큼 더해주고,
        // isActive와 liveTurret 배열도 갱신해줍니다.
        Turret weakTurret = liveTurret.get(0);
        int x = weakTurret.x;
        int y = weakTurret.y;
    
        board[x][y] += n + m;
        rec[x][y] = turn;
        weakTurret.p = board[x][y];
        weakTurret.r = rec[x][y];
        isActive[x][y] = true;
    
        liveTurret.set(0, weakTurret);
    }
    
    // 레이저 공격을 진행합니다.
    public static boolean laserAttack() {
        // 기존에 정렬된 가장 앞선 포탑이
        // 각성한 포탑입니다.
        Turret weakTurret = liveTurret.get(0);
        int sx = weakTurret.x;
        int sy = weakTurret.y;
        int pow = weakTurret.p;
    
        // 기존에 정렬된 가장 뒤 포탑이
        // 각성한 포탑을 제외한 포탑 중 가장 강한 포탑입니다.
        Turret strongTurret = liveTurret.get(liveTurret.size() - 1);
        int ex = strongTurret.x;
        int ey = strongTurret.y;
    
        // bfs를 통해 최단경로를 관리해줍니다.
        Queue&amp;lt;Pair&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
        vis[sx][sy] = true;
        q.add(new Pair(sx, sy));
    
        // 가장 강한 포탑에게 도달 가능한지 여부를 canAttack에 관리해줍니다.
        boolean canAttack = false;
    
        while(!q.isEmpty()) {
            int x = q.peek().x;
            int y = q.peek().y;
            q.poll();
    
            // 가장 강한 포탑에게 도달할 수 있다면
            // 바로 멈춥니다.
            if(x == ex &amp;amp;&amp;amp; y == ey) {
                canAttack = true;
                break;
            }
    
            // 각각 우, 하, 좌, 상 순서대로 방문하며 방문 가능한 포탑들을 찾고
            // queue에 저장해줍니다.
            for(int dir = 0; dir &amp;lt; 4; dir++) {
                int nx = (x + dx[dir] + n) % n;
                int ny = (y + dy[dir] + m) % m;
    
                // 이미 방문한 포탑이라면 넘어갑니다.
                if(vis[nx][ny]) 
                    continue;
    
                // 벽이라면 넘어갑니다.
                if(board[nx][ny] == 0) 
                    continue;
    
                vis[nx][ny] = true;
                backX[nx][ny] = x;
                backY[nx][ny] = y;
                q.add(new Pair(nx, ny));
            }
        }
    
        // 만약 도달 가능하다면 공격을 진행합니다.
        if(canAttack) {
            // 우선 가장 강한 포탑에게는 pow만큼의 공격을 진행합니다.
            board[ex][ey] -= pow;
            if(board[ex][ey] &amp;lt; 0) 
                board[ex][ey] = 0;
            isActive[ex][ey] = true;
    
            // 기존의 경로를 역추적하며
            // 경로 상에 있는 모든 포탑에게 pow / 2만큼의 공격을 진행합니다.
            int cx = backX[ex][ey];
            int cy = backY[ex][ey];
    
            while(!(cx == sx &amp;amp;&amp;amp; cy == sy)) {
                board[cx][cy] -= pow / 2;
                if(board[cx][cy] &amp;lt; 0) 
                    board[cx][cy] = 0;
                isActive[cx][cy] = true;
    
                int nextCx = backX[cx][cy];
                int nextCy = backY[cx][cy];
    
                cx = nextCx;
                cy = nextCy;
            }
        }
    
        // 공격을 성공했는지 여부를 반환합니다.
        return canAttack;
    }
    
    // 레이저 공격을 하지 못했다면 폭탄 공격을 진행합니다.
    public static void bombAttack() {
        // 기존에 정렬된 가장 앞선 포탑이
        // 각성한 포탑입니다.
        Turret weakTurret = liveTurret.get(0);
        int sx = weakTurret.x;
        int sy = weakTurret.y;
        int pow = weakTurret.p;
    
        // 기존에 정렬된 가장 뒤 포탑이
        // 각성한 포탑을 제외한 포탑 중 가장 강한 포탑입니다.
        Turret strongTurret = liveTurret.get(liveTurret.size() - 1);
        int ex = strongTurret.x;
        int ey = strongTurret.y;
    
        // 가장 강한 포탑의 3 * 3 범위를 모두 탐색하며
        // 각각에 맞는 공격을 진행합니다.
        for(int dir = 0; dir &amp;lt; 9; dir++) {
            int nx = (ex + dx2[dir] + n) % n;
            int ny = (ey + dy2[dir] + m) % m;
    
            // 각성한 포탑 자기 자신일 경우 넘어갑니다.
            if(nx == sx &amp;amp;&amp;amp; ny == sy) 
                continue;
    
            // 가장 강한 포탑일 경우 pow만큼의 공격을 진행합니다.
            if(nx == ex &amp;amp;&amp;amp; ny == ey) {
                board[nx][ny] -= pow;
                if(board[nx][ny] &amp;lt; 0) 
                    board[nx][ny] = 0;
                isActive[nx][ny] = true;
            }
            // 그 외의 경우 pow / 2만큼의 공격을 진행합니다.
            else {
                board[nx][ny] -= pow / 2;
                if(board[nx][ny] &amp;lt; 0) 
                    board[nx][ny] = 0;
                isActive[nx][ny] = true;
            }
        }
    }
    
    // 공격에 관여하지 않은 모든 살아있는 포탑의 힘을 1 증가시킵니다.
    public static void reserve() {
        for(int i = 0; i &amp;lt; n; i++) {
            for(int j = 0; j &amp;lt; m; j++) {
                if(isActive[i][j]) 
                    continue;
                if(board[i][j] == 0) 
                    continue;
                board[i][j]++;
            }
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        // 입력:
        n = sc.nextInt();
        m = sc.nextInt();
        k = sc.nextInt();
        for(int i = 0; i &amp;lt; n; i++)
            for(int j = 0; j &amp;lt; m; j++)
                board[i][j] = sc.nextInt();

        // k턴 동안 진행됩니다.
        while(k-- &amp;gt; 0) {
            // 턴을 진행하기 전 살아있는 포탑을 정리합니다.
            liveTurret = new ArrayList&amp;lt;&amp;gt;();
            for(int i = 0; i &amp;lt; n; i++)
                for(int j = 0; j &amp;lt; m; j++)
                    if(board[i][j] &amp;gt; 0) {
                        Turret newTurret = new Turret(i, j, rec[i][j], board[i][j]);
                        liveTurret.add(newTurret);
                    }

            // 살아있는 포탑이 1개 이하라면 바로 종료합니다.
            if(liveTurret.size() &amp;lt;= 1) 
                break;

            // 턴을 진행하기 전 필요한 전처리를 정리해줍니다.
            init();

            // 각성을 진행합니다.
            awake();

            // 레이저 공격을 진행합니다.
            boolean isSuc = laserAttack();
            // 레이저 공격을 하지 못했다면 포탄 공격을 진행합니다.
            if(!isSuc) 
                bombAttack();

            // 공격에 관여하지 않은 모든 살아있는 포탑의 힘을 1 증가시킵니다.
            reserve();
        }

        // 살아있는 포탑의 힘 중 가장 큰 값을 출력합니다.
        int ans = 0;
        for(int i = 0; i &amp;lt; n; i++)
            for(int j = 0; j &amp;lt; m; j++)
                ans = Math.max(ans, board[i][j]);

        Systehttp://m.out.print(ans);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디버깅 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. int dx int dy 경계 처리 잘 하기 (모서리 넘어갈 때)&lt;br /&gt;&lt;b&gt;2.&amp;nbsp;모든&amp;nbsp;포탑&amp;nbsp;공격력&amp;nbsp;0&amp;nbsp;시에&amp;nbsp;턴&amp;nbsp;더&amp;nbsp;진행하지&amp;nbsp;않고&amp;nbsp;턴&amp;nbsp;종료&lt;/b&gt;&lt;br /&gt;3.&amp;nbsp;레이저&amp;nbsp;공격&amp;nbsp;루트에서&amp;nbsp;공격자&amp;nbsp;제외&lt;br /&gt;&lt;b&gt;4.&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;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. 객체 비교할 일이 있을 때는 Comparable 정의하기.. (인생이 편해질듯)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744188273344&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// class turret을 정의해 관리합니다.
class Turret implements Comparable&amp;lt;Turret&amp;gt; {
    int x, y, r, p;

    public Turret(int x, int y, int r, int p) {
        this.x = x;
        this.y = y;
        this.r = r;
        this.p = p;
    }

    // turret의 약함, 강함 우선순위에 맞게 정렬함수를 만들어줍니다.
    public int compareTo(Turret t) {
        if(this.p != t.p) return this.p - t.p;
        if(this.r != t.r) return t.r - this.r;
        if(this.x + this.y != t.x + t.y) return (t.x + t.y) - (this.x + this.y);
        return t.y - this.y;
    }
};&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;</description>
      <category>PS</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/91</guid>
      <comments>https://oxdjww.tistory.com/91#entry91comment</comments>
      <pubDate>Wed, 9 Apr 2025 17:48:47 +0900</pubDate>
    </item>
    <item>
      <title>[Focussu] 아키텍쳐 설계</title>
      <link>https://oxdjww.tistory.com/90</link>
      <description>&lt;h2 data-end=&quot;108&quot; data-start=&quot;80&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;0. 온라인 스터디 플랫폼, Focussu 백엔드 아키텍처 설계기&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;159&quot; data-start=&quot;109&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;― 실시간 스트리밍, Kafka, 그리고 Docker Compose까지&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;캡스톤 프로젝트에서 &lt;b&gt;&quot;실시간 집중도 분석이 가능한 온라인 스터디 플랫폼&quot;&lt;/b&gt;을 개발하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;단순히 영상 전화를 넘어서, &lt;b&gt;각 사용자들의 스트리밍 데이터를 AI 서버로 전달하고&lt;/b&gt;, 그 분석 결과를 기반으로 &lt;b&gt;각 사람의 집중도를 실시간으로 보여주는 플랫폼&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;426&quot; data-start=&quot;316&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 글은 백엔드 개발자로서 내가 어떤 기술을 선택했고, 왜 그런 선택을 하게 되었는지를 정리한 기록이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;426&quot; data-start=&quot;316&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;또, 각종 트러블 슈팅을 시리즈 별로 정리할 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-end=&quot;431&quot; data-start=&quot;428&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;449&quot; data-start=&quot;433&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 핵심 요구사항 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;677&quot; data-start=&quot;451&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;498&quot; data-start=&quot;451&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;사용자의 비디오/오디오 스트리밍 데이터를 AI 분석 서버로 보내야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;499&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;따라서 WebRTC의 P2P 방식은 사용할 수 없고, &lt;b&gt;중계 서버(SFU 기반)&lt;/b&gt;가 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;611&quot; data-start=&quot;557&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인증/인가(Spring Security)는 비즈니스 로직과 분리해서 깔끔하게 구성해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;677&quot; data-start=&quot;612&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;AI 분석 결과는 Kafka를 통해 비동기적으로 받아야 하며, &lt;b&gt;백엔드에서 consume 후 DB에 저장&lt;/b&gt;해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1.1 아키텍쳐(초안)&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7TN8E/btsNa5AmsVi/1GRlySKYCSBgdHQOpDSLK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7TN8E/btsNa5AmsVi/1GRlySKYCSBgdHQOpDSLK0/img.png&quot; data-alt=&quot;수행 계획서에 첨부한 초안이며, 실제로는 NoSQL을 쓸지도 고민 중에 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7TN8E/btsNa5AmsVi/1GRlySKYCSBgdHQOpDSLK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7TN8E%2FbtsNa5AmsVi%2F1GRlySKYCSBgdHQOpDSLK0%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;612&quot; height=&quot;404&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1054&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수행 계획서에 첨부한 초안이며, 실제로는 NoSQL을 쓸지도 고민 중에 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;682&quot; data-start=&quot;679&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;706&quot; data-start=&quot;684&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 기술 스택 선정과 고민의 흔적&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;741&quot; data-start=&quot;708&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.1 &lt;b&gt;Mediasoup &amp;ndash; 스트리밍 중계 서버&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;856&quot; data-start=&quot;743&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음엔 WebRTC를 활용해서 P2P 구조로 만들까도 생각했다. 하지만 이 구조는 &lt;b&gt;클라이언트끼리 직접 연결&lt;/b&gt;되기 때문에, 중간에서 데이터를 가로채서 &lt;b&gt;AI 분석 서버로 전달&lt;/b&gt;하는 게 불가능했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;918&quot; data-start=&quot;858&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래서 &lt;b&gt;서버 기반의 SFU 구조&lt;/b&gt;로 방향을 틀었고, 여러 옵션 중에서 &lt;b&gt;mediasoup&lt;/b&gt;를 선택했다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-end=&quot;1097&quot; data-start=&quot;920&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1097&quot; data-start=&quot;922&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;mediasoup는 Node.js 기반 SFU로, 커스터마이징 자유도도 높고 성능도 괜찮다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;가장 좋았던 점은 &lt;b&gt;서버가 모든 클라이언트의 stream을 받아 중계&lt;/b&gt;해줄 수 있다는 점이었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시에, 이 stream을 &lt;b&gt;그대로 AI 서버에 포워딩&lt;/b&gt;할 수 있어 요구사항과 완벽히 맞아떨어졌다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-end=&quot;1102&quot; data-start=&quot;1099&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1145&quot; data-start=&quot;1104&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.2 &lt;b&gt;Kafka &amp;ndash; 집중도 분석 결과 수신을 위한 메시지 큐&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1260&quot; data-start=&quot;1147&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;AI 분석 서버는 실시간 스트리밍 데이터를 받아 집중도를 분석한다. 그런데 이 결과를 &lt;b&gt;HTTP로 받아 처리하는 건 너무 비효율적&lt;/b&gt;이고, 분석 시간이 일정하지 않아 &lt;b&gt;비동기 구조가 필수적&lt;/b&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1278&quot; data-start=&quot;1262&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래서 Kafka를 도입했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1384&quot; data-start=&quot;1280&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1328&quot; data-start=&quot;1280&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;AI 분석 서버&lt;/b&gt;는 Kafka topic에 집중도 결과를 발행(publish)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1384&quot; data-start=&quot;1329&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Spring Boot 백엔드&lt;/b&gt;는 Kafka를 구독(consume)하고, 결과를 DB에 저장&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-end=&quot;1479&quot; data-start=&quot;1386&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1479&quot; data-start=&quot;1388&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka 덕분에 AI 서버와 백엔드가 완전히 &lt;b&gt;decoupling&lt;/b&gt;되었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처리량이 많아져도 쉽게 스케일링할 수 있고, 분석 지연에도 유연하게 대응 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-end=&quot;1484&quot; data-start=&quot;1481&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1526&quot; data-start=&quot;1486&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.3 &lt;b&gt;Spring Boot &amp;ndash; 인증과 비즈니스 로직의 허브&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1553&quot; data-start=&quot;1528&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Boot는 다음 역할들을 맡는다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1711&quot; data-start=&quot;1555&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1577&quot; data-start=&quot;1555&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;JWT 기반 사용자 인증 처리&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1620&quot; data-start=&quot;1578&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;스터디룸 입장 요청 시, 중계 서버에 요청을 보내 &lt;b&gt;접속 정보 생성&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1670&quot; data-start=&quot;1621&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 정보를 사용자에게 전달해, &lt;b&gt;스트리밍 서버 연결까지 연결&lt;/b&gt;되는 흐름을 만든다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1711&quot; data-start=&quot;1671&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka 구독 후 집중도 데이터를 DB에 저장하는 역할도 수행한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;TDD로 개발하여, 단위 테스트 별로 안전하게 검증된 API를 만들어보고자 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-end=&quot;1830&quot; data-start=&quot;1827&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1860&quot; data-start=&quot;1832&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. Docker Compose로 인프라 구성&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1933&quot; data-start=&quot;1862&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 모든 걸 안정적으로 구동시키기 위해 Docker Compose를 사용해서 전체 인프라를 컨테이너화했다. 구조는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744004480930&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;├── docker-compose.yml
├── backend                # Spring Boot 서버
│   ├── Dockerfile
│   └── src, build, ...
├── mediasoup-server       # Node.js 기반 SFU 서버 (별도 디렉토리 존재)
└── kafka&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2503&quot; data-start=&quot;2229&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2300&quot; data-start=&quot;2229&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;backend&lt;/b&gt;: JWT 인증, 스터디룸 입장 처리, Kafka consumer, DB 연동 등 비즈니스 로직 담당&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2369&quot; data-start=&quot;2301&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;mediasoup-server&lt;/b&gt;: 클라이언트 스트림을 받아 다른 사용자들에게 중계, AI 서버로 스트림 포워딩&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2467&quot; data-start=&quot;2429&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;kafka + zookeeper&lt;/b&gt;: 비동기 메시지 브로커&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-end=&quot;2623&quot; data-start=&quot;2505&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;2623&quot; data-start=&quot;2507&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Docker Compose 덕분에 로컬 환경에서도 전체 시스템을 &lt;b&gt;한 줄로 띄울 수 있어서 개발이 수월&lt;/b&gt;했고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이후 배포 시에도 docker swarm이나 k8s로 자연스럽게 확장 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&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;pre id=&quot;code_1744030989705&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sh rerun.sh&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1744030963030&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/sh

echo &quot;Stopping and removing containers, networks, volumes...&quot;
docker compose down -v

echo &quot;Starting containers in detached mode...&quot;
docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;2628&quot; data-start=&quot;2625&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2644&quot; data-start=&quot;2630&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. 실제 흐름 요약&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2993&quot; data-start=&quot;2646&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2698&quot; data-start=&quot;2646&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;클라이언트가 Spring Boot 서버로 &lt;b&gt;JWT 기반 인증 + 스터디룸 입장 요청&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2746&quot; data-start=&quot;2699&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Boot 서버가 mediasoup 서버에 &lt;b&gt;접속 정보 요청&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2771&quot; data-start=&quot;2747&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;생성된 접속 정보를 클라이언트에게 반환&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2813&quot; data-start=&quot;2772&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;클라이언트는 mediasoup에 접속하고, &lt;b&gt;스트림을 업로드&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2875&quot; data-start=&quot;2814&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;mediasoup는 이 스트림을 &lt;b&gt;다른 사용자들에게 중계&lt;/b&gt;, 동시에 &lt;b&gt;AI 분석 서버로 전달&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2913&quot; data-start=&quot;2876&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;분석 서버는 집중도 분석 후, &lt;b&gt;Kafka에 메시지 전송&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2964&quot; data-start=&quot;2914&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Boot 서버는 Kafka를 구독해서 메시지를 받고, &lt;b&gt;DB에 저장&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2993&quot; data-start=&quot;2965&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용자 요청 시 집중도 데이터를 API로 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;2998&quot; data-start=&quot;2995&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3034&quot; data-start=&quot;3000&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. 마치며:&amp;nbsp; &quot;설계는 전략이다&quot;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;3077&quot; data-start=&quot;3036&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 프로젝트 초기 아키텍쳐 설계를 하고, 각 요구사항을 반영하는 과정을 통해 &quot;아키텍처는 전략이다&quot;라는 말을 다시 한 번 느꼈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;3171&quot; data-start=&quot;3079&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기술 하나하나가 서로 잘 맞물려 돌아가도록 설계하고, 문제가 생겨도 쉽게 디버깅하고 확장할 수 있게 만드는 것.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그게 진짜 서버 개발자의 역할이라는 걸 느꼈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;3235&quot; data-start=&quot;3173&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3235&quot; data-start=&quot;3173&quot; data-ke-size=&quot;size16&quot;&gt;하나의 기술을 선택할 때 많은 고민과 설계를 하는 노력이, 러프한 설계 후 개발하며 하는 고생보다 낫다!&lt;/p&gt;</description>
      <category>Activities/Focussu 개발일지</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/90</guid>
      <comments>https://oxdjww.tistory.com/90#entry90comment</comments>
      <pubDate>Mon, 7 Apr 2025 22:04:16 +0900</pubDate>
    </item>
    <item>
      <title>Ne(o)rdinary Hackerthon 회고</title>
      <link>https://oxdjww.tistory.com/84</link>
      <description>&lt;h1&gt;행사 개요&lt;/h1&gt;
&lt;p&gt;Ne(o)rdinary Hackerthon이란&lt;br&gt;UMC 14기, CMC 5기의 크루원들이 모여 1박 2일간 주제를 갖고 개발을 진행하는 해커톤이다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;다른 프로젝트도 진행하고 있고, 그 외에도 일정이 있어 사실 해커톤 참여를 망설였다.&lt;br&gt;하루 밤 새는 것이 타격이 크기 때문에 주어진 일을 못 할까 걱정이 앞섰기 때문이다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;또한, 제대로된 해커톤이 사실상 처음이라 짧은 시간 내에 개발할 수 있을지, 팀원분들에게 폐를 끼치진 않을지 걱정했다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;하지만 짧은 시간동안 좋은 팀원분들과 개발에 몰두하며 많은 것을 배울 수 있었고,&lt;br&gt;짧은 시간이지만 해커톤에 몰입한 결과 만족스러운 결과도 얻을 수 있었다!&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;주제&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;뉴진스(New Jeans)의 노래 제목을 앱 이름으로 하여 서비스 구상&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;“뉴진스의 하입보이요”는 2023년 대표 밈 중 하나입니다.
새로운 청바지라는 이름을 가진 이 걸그룹은 현 1020 세대에 굉장한 영향력을 미치고, 틱톡 등 숏폼에 익숙한 Z세대는 밈을 생산하며 트렌드를 주도합니다.

2024년 트렌드 코리아의 키워드로써 선정된 Ditto를 포함하여 Hype boy,
”코카콜라 맛있다”로 코카콜라 제로를 각인시킨 Zero 등 뉴진스의 노래 제목에서 소비자를 매료시킬 수 있는 서비스를 탄생시키는 것이 이번 해커톤의 목표입니다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;올 한해 가장 핫했던 밈 중 하나는 뉴진스의 노래인 하입보이라고 할 수 있다.&lt;br&gt;그렇기에 노래 제목 중 하나를 주제로 하는 서비스를 개발해보는 것이 이번 해커톤의 주제다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;사실 사회문제나 정형화된 키워드를 던져줄 줄 알았는데, 이런 주제일 줄은 상상하지 못 해서 조금은 당황스러웠다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h1&gt;진행&lt;/h1&gt;
&lt;h2&gt;팀 매칭&lt;/h2&gt;
&lt;p&gt;해커톤 장소인 프론트원(공덕역 4번출구) 5층에 도착하였다.&lt;br&gt;많은 인원이 모였고, 지정된 자리에 착석했다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;10개의 팀, 100여명의 인원이 있었고,&lt;br&gt;앉은 테이블에 있던 분들이 우리 팀이였다..&lt;br&gt;앉자마자 왁자지껄 담소를 나누시던 우리 팀원들이 아직 눈에 선명하다..&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;아이디어 브레인 스토밍 &lt;code&gt;(10:00 - 12:00)&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;해커톤 주제가 발표되고, 이를 바탕으로 아이디어 디벨롭하는 과정을 가졌다.&lt;br&gt;우리팀은 기획자 2, 디자이너 1, BE 개발자 3, FE 개발자 3 이렇게 9명으로 구성되어 있었다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&amp;#39;구현 가능할지&amp;#39; 보다는 주제에 맞게 figma에 포스트잇을 붙이고, 최대한 많은 아이디어를 내며 짧은 시간 내에 괜찮은 아이디어가 나올지 다들 고민했다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;아이디어 디벨롭&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v2f0F/btsBjQ7Sz7H/ePQKe9OTzBsSCANpUtL9S1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v2f0F/btsBjQ7Sz7H/ePQKe9OTzBsSCANpUtL9S1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v2f0F/btsBjQ7Sz7H/ePQKe9OTzBsSCANpUtL9S1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv2f0F%2FbtsBjQ7Sz7H%2FePQKe9OTzBsSCANpUtL9S1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;뉴진스의 노래를 리스트업하고 노래 제목에 걸맞은 주제를 하나하나 내다 보니, 다양한 의견들이 나왔다.&lt;br&gt;그 중 세 가지 아이디어를 디벨롭했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;해몽 풀이 서비스&lt;/li&gt;
&lt;li&gt;대학원생 커뮤니티, 정보 플랫폼&lt;/li&gt;
&lt;li&gt;자기소개 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;주제(노래 제목)에 맞는 서비스이고, 현실적으로 구현이 가능할지, 그리고 기존에 있는 서비스라면 차별화된 기능이 있을지&lt;br&gt;다양한 요소를 고민한 끝에, 자기소개 서비스로 주제가 굳어졌다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;미리보기&amp;gt;&lt;/strong&gt;&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bedq1/btsBgvYfct0/0egmEKSyjUKjWLyEvvMm7K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bedq1/btsBgvYfct0/0egmEKSyjUKjWLyEvvMm7K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bedq1/btsBgvYfct0/0egmEKSyjUKjWLyEvvMm7K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBedq1%2FbtsBgvYfct0%2F0egmEKSyjUKjWLyEvvMm7K%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;그리고 뉴진스의 노래 중 하나인 &lt;code&gt;Attention&lt;/code&gt;으로 서비스명을 정하게 되었다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;개발 &lt;code&gt;(14:00 - 10:00)&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;점심 식사를 마치고, 각 직군별로 작업에 들어갔다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;서버 파트는 노션 페이지에 ERD와 API 명세를 짜는 것을 우선으로 하였다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;나는 ERD를 짜고, 한 분은 API 명세를 짜셨고, 한 분은 도커를 통해 CI/CD 파이프라인을 구축하셨다.&lt;br&gt;그리고 서버 파트원들 중 한 분이 로그인/회원가입을 미리 구현해오셔서 너무너무 든든했다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;설계된 ERD와 API 명세를 기반으로 개발에 들어갔고,&lt;br&gt;사실 주어진 시간 내에 하려다보니 중간중간 멘탈이 조금 나갔었다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;인프런 강의를 통해 클론코딩만 하던 나에게.. 비즈니스 요구사항에 맞게, 또 심지어는 타임 리밋이 있는 상황에서&lt;br&gt;개발을 한다는 자체가 압박감이 있었다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;하지만 모르는 상황에서 아는 척하고 진전없이 있는 것은 진짜 아니라고 생각되었다.&lt;br&gt;그래서 모르는 것들을 솔직하게 말하고 팀원들에게 바로바로 물어보고, 개발하였다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;각자 할일에 바빴을텐데도 끝까지 같이 힘내준 서버 팀원들에게 무한한 감사를 .. 표한다..&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;완성한 기능을 swagger를 통해 테스트하고, dev 브랜치에 push하는 일련의 과정을 반복하여 개발을 진행하였다.&lt;br&gt;카드 삭제/조회 기능에서 사용자의 권한에 대해 다룰 수 있었다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;그런데 swagger에서 아무리 시도를 해도 카드를 조회할 수 없는 500대 에러가 나오는 것이었다.&lt;br&gt;이 문제를 가지고 코드와 로그를 잡고 2시간을 보았고, 결국 팀원분의 도움을 받아 해결하였다.&lt;br&gt;api에서 User를 주입 받아야 권한이 계속 유지된다는 뜻으로 이해하였는데, 더 공부해볼 것이다!!!&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;그러다가 새벽에 라면도 먹으러 갔다..&lt;br&gt;하하..&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOeTTH/btsBgaNGSTj/61mI5p94J7s66LUV5Ogku1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOeTTH/btsBgaNGSTj/61mI5p94J7s66LUV5Ogku1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOeTTH/btsBgaNGSTj/61mI5p94J7s66LUV5Ogku1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOeTTH%2FbtsBgaNGSTj%2F61mI5p94J7s66LUV5Ogku1%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;우리 서비스의 핵심 기능인 카드/지갑 만들기 구현이 끝나고,&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEOUU0/btsBjQfKZ1R/NsX2pgRGtwK9X2rytx0KIk/tfile.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEOUU0/btsBjQfKZ1R/NsX2pgRGtwK9X2rytx0KIk/tfile.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEOUU0/btsBjQfKZ1R/NsX2pgRGtwK9X2rytx0KIk/tfile.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEOUU0%2FbtsBjQfKZ1R%2FNsX2pgRGtwK9X2rytx0KIk%2Ftfile.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;ChatGPT OpenAPI를 통해서 사용자에게 어울리는 색을 추천하여 카드의 색을 칠하는 기능까지 구현 완료하였다.&lt;br&gt;그러다보니 시간은 어느덧 새벽 5시가 되었고.. 다들 개발 마무리 단계에 들어섰다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;평가 및 시상식 (10:00 - 14:00)&lt;/h2&gt;
&lt;p&gt;개발 마무리 후 각종 자료들을 제출했다.&lt;br&gt;그리고 자리를 정리하고, 평가를 진행했다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMhVoU/btsBjCvcbmm/FKm9RojGicQQtax5mpzMS0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMhVoU/btsBjCvcbmm/FKm9RojGicQQtax5mpzMS0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMhVoU/btsBjCvcbmm/FKm9RojGicQQtax5mpzMS0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMhVoU%2FbtsBjCvcbmm%2FFKm9RojGicQQtax5mpzMS0%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;각 팀의 PM이 나와 서비스를 발표했고, 평가 기준은 아이디어, 기획, 디자인, 클라이언트, 서버 각 20점으로 총 100점 만점이였다.&lt;br&gt;세부 항목으로는 완성도, 기술, 심미성, 편리성, 배포여부, 창의성, 적합성 등이 있었다!&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc4J23/btsBjenNdTQ/KyuK6rJ7Pwbh1SLX0mJ8I1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc4J23/btsBjenNdTQ/KyuK6rJ7Pwbh1SLX0mJ8I1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc4J23/btsBjenNdTQ/KyuK6rJ7Pwbh1SLX0mJ8I1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc4J23%2FbtsBjenNdTQ%2FKyuK6rJ7Pwbh1SLX0mJ8I1%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;그리구 우리팀 발표!&lt;br&gt;발표 정말정말 잘하신다..&lt;br&gt;발표를 하면서, 멘토님이 피드백을 해주시는데 평가 뿐만 아니라 주제 적합도, 비즈니스 가능성, 기술적인 질문 등 다양하고 세부적으로 해주셔서 감사했다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;그리고 시상!!&lt;br&gt;정말 감사하게도, 10개 팀 중 2등으로 최우수상을 수상하였다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSnEfF/btsBgLGTu8B/TaOlKdqVyC2Zjl8Wxk1zw0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSnEfF/btsBgLGTu8B/TaOlKdqVyC2Zjl8Wxk1zw0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSnEfF/btsBgLGTu8B/TaOlKdqVyC2Zjl8Wxk1zw0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSnEfF%2FbtsBgLGTu8B%2FTaOlKdqVyC2Zjl8Wxk1zw0%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbZmMk/btsBjrgeOlK/ncgyMjOIUjOvxGFGQs7pLk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbZmMk/btsBjrgeOlK/ncgyMjOIUjOvxGFGQs7pLk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbZmMk/btsBjrgeOlK/ncgyMjOIUjOvxGFGQs7pLk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbZmMk%2FbtsBjrgeOlK%2FncgyMjOIUjOvxGFGQs7pLk%2Fimg.jpg&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br&gt;

&lt;h1&gt;마치며&lt;/h1&gt;
&lt;p&gt;기대하지 않았는데 좋은 결과 맺게 되어 너무 기분이 좋았다.&lt;br&gt;좋은 팀원들 만나서 낮부터 밤까지 기쁨과 즐거움과 힘듦과 정신나감과 성취감을 느껴 보람찼다!&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;기술적으로 많이 부족한 나에게 이번 수상은 큰 동기부여가 되었다.&lt;br&gt;내가 알고 있던 지식이 정말 얕고, 부족했다는 것을 느꼈고&lt;br&gt;이를 바탕으로 더 성장할 수 있으리라 생각되었다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;팀원 중 한 분이&lt;br&gt;해커톤은 개발 실력을 나 스스로 점검할 수 있는 기회라고 생각한다고 하셨었는데,&lt;br&gt;매우매우 동감한다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;추가로,&lt;br&gt;주어진 시간 내에 많은 인원이 하나의 목표를 갖고 열심히 일하는 것 자체가 매력적으로 다가왔다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;앞으로 해커톤에 또 참여하게 된다면, 그 때는 더 성장하여서 더 좋은 품질의 코드를 작성하고,&lt;br&gt;팀원들에게 더 도움이 될 수 있는 개발자로 참여하고 싶다!&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;필자는 해커톤 참여를 망설이는 분들께 강력히 추천을 권하는 바이다.&lt;br&gt;할 수 있씁니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/2023-Attention-Hackerthon/server/tree/main&quot;&gt;Git Repo&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;</description>
      <category>Activities/UMC 5th</category>
      <category>CMC</category>
      <category>IT연합동아리</category>
      <category>neordinary</category>
      <category>Spring Security</category>
      <category>UMC</category>
      <category>개발 동아리</category>
      <category>백엔드 개발</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>해커톤</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/84</guid>
      <comments>https://oxdjww.tistory.com/84#entry84comment</comments>
      <pubDate>Sat, 2 Dec 2023 01:54:36 +0900</pubDate>
    </item>
    <item>
      <title>연관관계 매핑(양방향)</title>
      <link>https://oxdjww.tistory.com/83</link>
      <description>&lt;div class='markdown-body'&gt;

&lt;h1&gt;✅ 양방향 매핑이란?&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/du0uuH/btsAFFlYEWr/jBNL66F23j7kGtftrKx1M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/du0uuH/btsAFFlYEWr/jBNL66F23j7kGtftrKx1M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/du0uuH/btsAFFlYEWr/jBNL66F23j7kGtftrKx1M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdu0uuH%2FbtsAFFlYEWr%2FjBNL66F23j7kGtftrKx1M0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;객체의 연관관계 중 하나인 양방향 매핑은, 사실상 단방향 매핑이 두번 이루어진 것이다.&lt;/li&gt;
&lt;li&gt;즉, 개념적으로 두 개의 단방향 매핑을 추상적으로 양방향 매핑이라 칭하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ 양방향 연관관계 매핑의 필요성&lt;/h1&gt;
&lt;p&gt;테이블과 객체를 비교해보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;테이블에서는 외래 키 하나로 두 테이블의 연관관계를 확인할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외래 키를 가지고 조인하면 두 테이블간 데이터의 결합과 접근성이 자유롭다.&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;li&gt;참조가 양방향으로 존재해야 참조와 역참조가 가능한 구조라는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;이런 패러다임의 차이를 극복하기 위해 양방향 매핑을 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;하지만, 엔티티 연관관계를 설정할 때 우선 단방향 매핑으로 구성하되, 비즈니스 로직상 역참조가 필요할 경우에 양방향 매핑을 해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;불필요한 양방향 매핑은, 로직의 복잡성을 증가시키고 문제 발생 시 원인을 찾기 어렵게 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  구체적으로 어떤 경우?&lt;/h2&gt;
&lt;p&gt;예를 들어..&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Order&lt;/code&gt;와 &lt;code&gt;OrderItem&lt;/code&gt;이 존재할 때, 이는 &lt;code&gt;1:N&lt;/code&gt; 관계이므로 외래키가 &lt;code&gt;OrderItem&lt;/code&gt;에 존재한다.&lt;/li&gt;
&lt;li&gt;하지만, 일반적으로 주문(&lt;code&gt;Order&lt;/code&gt;)에서 주문한 상품의 목록(&lt;code&gt;OrderItem&lt;/code&gt; 여러개)을 조회하는 일이 잦기에 이를 &lt;code&gt;List&amp;lt;OrderItem&amp;gt;&lt;/code&gt;로 구현하고자 한다.&lt;/li&gt;
&lt;li&gt;위는 단순한 예시일 뿐이고, 우리는 단방향 매핑에서 사용했던 예제인 &lt;code&gt;Member&lt;/code&gt;와 &lt;code&gt;Team&lt;/code&gt;의 경우를 이어서 학습한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ &lt;code&gt;Member&lt;/code&gt; &amp;amp; &lt;code&gt;Team&lt;/code&gt;&lt;/h1&gt;
&lt;h2&gt;  엔티티 매핑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Member&lt;/code&gt;는 동일하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;      @Entity

  public class Member {
      @Id @GeneratedValue
      private Long id;

      @Column(name = &amp;quot;USERNAME&amp;quot;)
      private String name;
      private int age;

      // @Column(name = &amp;quot;TEAM_ID&amp;quot;)
      // private Long teamId;

      @ManyToOne
      @JoinColumn(name = &amp;quot;TEAM_ID&amp;quot;)
      private Team team;
      ...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Team&lt;/code&gt;에는, &lt;code&gt;Member&lt;/code&gt; 여러개를 관리하는 컬렉션 &lt;code&gt;List&lt;/code&gt;를 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  @Entity
  public class Team {
      @Id @GeneratedValue
      private Long id;

      private String name;

      @OneToMany(mappedBy = &amp;quot;team&amp;quot;)
      List&amp;lt;Member&amp;gt; members = new ArrayList&amp;lt;member&amp;gt;();
      ...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  엔티티 다루기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;이를 통해 역방향으로 객체 그래프를 탐색할 수 있는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  // 조회
  Team findteam = em.find(Team.class, team.getId());

  int memberSize = findTeam.getMembers().size(); // 역방향 조회&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ 연관관계의 주인&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mappedBy= owner&lt;/code&gt; 의미 : &lt;strong&gt;“내 주인은 &lt;code&gt;owner&lt;/code&gt;야”&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;연관관계의 주인이라는 개념이 양방향 매핑에서는 필요하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNKnOr/btsACokZHVV/m6JnsyBEvIWyYHFoSHxtdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNKnOr/btsACokZHVV/m6JnsyBEvIWyYHFoSHxtdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNKnOr/btsACokZHVV/m6JnsyBEvIWyYHFoSHxtdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNKnOr%2FbtsACokZHVV%2Fm6JnsyBEvIWyYHFoSHxtdK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외래 키를 관리해줄 주체가 필요해진 것이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Member&lt;/code&gt; 레코드에서, &lt;code&gt;TEAM_ID&lt;/code&gt;의 변경 기준은 무엇일까?&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Member&lt;/code&gt; 객체의 &lt;code&gt;Team team&lt;/code&gt;이 변경되었을 때?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Team&lt;/code&gt; 객체의 &lt;code&gt;List members&lt;/code&gt; 이 변경되었을 때?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 기준이 바로 &lt;strong&gt;주인(Owner)&lt;/strong&gt;이다.&lt;ul&gt;
&lt;li&gt;객체의 두 관계 중 하나를 연관관계의 주인으로 설정하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  양방향 매핑 규칙&lt;/h2&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;주인은 &lt;code&gt;mappedBy&lt;/code&gt; 사용 안함&lt;/li&gt;
&lt;li&gt;주인이 아니면 &lt;code&gt;mappedBy&lt;/code&gt; 로 주인 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  연관관계의 주인 기준&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;연관관계의 주인은 외래키쪽으로&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&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;code&gt;1:N&lt;/code&gt;에서 &lt;code&gt;N&lt;/code&gt;쪽이다. 역의 경우에서 모순을 찾아 증명해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;  외래키가 없는, &lt;code&gt;1:N&lt;/code&gt;에서 &lt;code&gt;1&lt;/code&gt;쪽이 주인이라면..&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1쪽이 주인이면, &lt;code&gt;Team&lt;/code&gt;에 외래 키가 존재하고, 멤버 여러명이 한 &lt;code&gt;Team&lt;/code&gt;에 속하는 구조일 것이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Team&lt;/code&gt;에 외래 키가 존재한다는 것은, &lt;code&gt;Member&lt;/code&gt;의 &lt;code&gt;PK&lt;/code&gt;인 &lt;code&gt;memberId&lt;/code&gt;를 갖는 레코드가 다수 있다는 뜻이 된다.&lt;/li&gt;
&lt;li&gt;그 뜻은 &lt;code&gt;member&lt;/code&gt;의 레코드 중 같은 팀인 &lt;code&gt;member&lt;/code&gt;는 &lt;code&gt;Team&lt;/code&gt;의 &lt;code&gt;PK&lt;/code&gt;값이 같을 것이고,&lt;/li&gt;
&lt;li&gt;그럼 유일해야 하는 &lt;code&gt;PK&lt;/code&gt;의 값에 중복이 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ 양방향 매핑 시 하는 실수&lt;/h1&gt;
&lt;h2&gt;  연관관계의 주인에 값을 입력하지 않는 경우&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Team team = new Team();
team.setName(&amp;quot;TeamA&amp;quot;);
em.persist(team);

Member member = new Member();
member.setName(&amp;quot;member1&amp;quot;);

//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);

em.persist(member);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;연관관계 주인이 아닌 쪽의 엔티티에 접근하여 &lt;code&gt;.add()&lt;/code&gt;를 한다.&lt;/li&gt;
&lt;li&gt;그렇게 되면 실제로 삽입된 &lt;code&gt;member&lt;/code&gt; 레코드의 &lt;code&gt;TEAM_ID&lt;/code&gt; 필드 값은 &lt;strong&gt;null&lt;/strong&gt;로 지정되게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그렇기에 연관관계의 주인에 값을 설정해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Team team = new Team();
team.setName(&amp;quot;TeamA&amp;quot;);
em.persist(team);

Member member = new Member();
member.setName(&amp;quot;member1&amp;quot;);

team.getMembers().add(member); //연관관계의 주인에 값 설정
member.setTeam(team); //**

em.persist(member);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;member.setTeam(team);&lt;/code&gt; 을 통해, 연관관계 주인의 컬럼에 값을 더해준 것을 볼 수 있다.&lt;/li&gt;
&lt;li&gt;이 코드가 있기 때문에, &lt;code&gt;team.getMembers().add(member);&lt;/code&gt; 코드는 사실상 주석처리를 해줄 수 있다.&lt;/li&gt;
&lt;li&gt;나중에 데이터베이스에 등록된 &lt;code&gt;team&lt;/code&gt;으로 &lt;code&gt;team.getMembers()&lt;/code&gt;를 사용할 때, 지연로딩에 의해 이 컬럼(&lt;code&gt;members&lt;/code&gt;)과 연관된 값들에 대해 &lt;code&gt;SELECT SQL&lt;/code&gt;을 한번 날리게 된다.&lt;/li&gt;
&lt;li&gt;그 때 &lt;code&gt;members&lt;/code&gt;에서 &lt;code&gt;TEAM_ID&lt;/code&gt;가 같은 값들을 조회하며 갱신되는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;  연관관계 편의 메서드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;순수 객체 상태를 고려, 혹은 객체지향적인 특징에 잘 맞추기 위해 항상 양쪽에 값을 설정하자.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;즉, 위의 예제를 빌어 말을 보충하자면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;member.setTeam(team);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;team.getMembers().add(member);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;이 두 가지를 모두 해주자는 것이다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;하지만 두 가지 작업을 모두 해주는 것은 사람이 실수를 할 수도 있기에, 메서드로 따로 구현하여 빼먹는 일이 없도록 하면 더 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  // 그 것을 연관관계 편의 메서드라 하며, 두 객체 중 한 쪽에만 구현하는 것이 관례이다.
  // team 내부의 함수. 멤버 변수로 List&amp;lt;member&amp;gt; members 존재.
  public void addMember(Member member) {
      members.add(member);
      member.setTeam(this);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Ref&lt;/h1&gt;
&lt;p&gt;김영한 강사님, JPA 프로그래밍 - 기본편&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;

&lt;/div&gt;</description>
      <category>Dev/Spring</category>
      <category>joincolumn</category>
      <category>JPA</category>
      <category>mappedBy</category>
      <category>SpringBoot</category>
      <category>객체</category>
      <category>데이터베이스 테이블</category>
      <category>양방향 매핑</category>
      <category>연관관계 매핑</category>
      <category>연관관계 이유</category>
      <category>연관관계 주인</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/83</guid>
      <comments>https://oxdjww.tistory.com/83#entry83comment</comments>
      <pubDate>Tue, 21 Nov 2023 01:49:47 +0900</pubDate>
    </item>
    <item>
      <title>연관관계 매핑(단방향)</title>
      <link>https://oxdjww.tistory.com/82</link>
      <description>&lt;div class='markdown-body'&gt;

&lt;h1&gt;✅ 연관관계 매핑의 필요성&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;모델링의 두 방식의 차이점을 비교하며 연관관계의 필요성을 알아보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ 테이블 중심 모델링&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z2vht/btsAG2uePSf/51AhUGTHy2HbTK0XqR15o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z2vht/btsAG2uePSf/51AhUGTHy2HbTK0XqR15o1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z2vht/btsAG2uePSf/51AhUGTHy2HbTK0XqR15o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ2vht%2FbtsAG2uePSf%2F51AhUGTHy2HbTK0XqR15o1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
![Untitled]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테이블 연관관계에 맞추어 객체를 모델링 해보자&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  엔티티 매핑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;참조 대신에 외래키를 그대로 사용하여 다음과 같이 &lt;code&gt;Member&lt;/code&gt; 와 &lt;code&gt;Team&lt;/code&gt; 을 작성할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  @Entity
  public class Member {
      @Id @GeneratedValue
      private Long id;

      @Column(name = &amp;quot;USERNAME&amp;quot;)
      private Long teamId;
      ...
  }
  @Entity
  public class Team {
      @Id @GeneratedValue
      private Long id;
      private String name;
      ...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  엔티티 다루기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;위와 같이 작성하게 되면, 실제로 엔티티를 다룰 때 아래와 같이 다루게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  // 팀 저장
  Team team = new Team();
  team.setName(&amp;quot;TeamA&amp;quot;);
  em.persist(team);

  //회원 저장
  Member member = new Member();
  member.setName(&amp;quot;member1&amp;quot;);
  member.setTeamId(team.getId());
  em.persist(member);

  //조회
  Member findMember = em.find(Member.class, member.getId());

  //연관관계가 없음
  Team findTeam = em.find(Team.class, team.getId());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;저장한 &lt;code&gt;Member&lt;/code&gt;와 &lt;code&gt;Team&lt;/code&gt; 에 대해 연관관계가 존재하지 않는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;그러므로 &lt;code&gt;findMember&lt;/code&gt;의 &lt;code&gt;team&lt;/code&gt;을 찾을 방법이 없다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;그저 이전에 생성한 &lt;code&gt;team&lt;/code&gt; 객체의 &lt;code&gt;team.getId()&lt;/code&gt;를 통해 식별자로 다시 조회한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;객체지향적인 방법이 아니다. 객체간의 관계를 코드로 풀어낼 수 없기 때문이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;객체가 행동을 하지 않고, &lt;code&gt;getter&lt;/code&gt;를 사용한 식별자로 엔티티를 조회한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  테이블 중심 모델링의 한계&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;테이블의 컬럼을 중심으로 객체를 모델링하게 되면, 협력 관계를 만들 수 없다.&lt;/li&gt;
&lt;li&gt;테이블 : 외래 키로 조인을 사용하여 연관된 테이블을 찾는다.&lt;/li&gt;
&lt;li&gt;객체 : 참조를 이용하여 연관된 객체를  찾는다.&lt;/li&gt;
&lt;li&gt;이런 간극을 극복해낼 방법 → &lt;strong&gt;연관 관계&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ 객체 지향 모델링&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpSjXf/btsAAQa0rzV/89ZVcihRhwxY4rxuoYWJw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpSjXf/btsAAQa0rzV/89ZVcihRhwxY4rxuoYWJw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpSjXf/btsAAQa0rzV/89ZVcihRhwxY4rxuoYWJw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpSjXf%2FbtsAAQa0rzV%2F89ZVcihRhwxY4rxuoYWJw1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;객체의 연관관계를 바탕으로 모델링해보자.&lt;/li&gt;
&lt;li&gt;테이블 중심 모델링과 다른 점은, &lt;code&gt;teamId&lt;/code&gt; → &lt;code&gt;Team team&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  엔티티 매핑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;기존에 썼던 teamId를 주석 처리하고, &lt;code&gt;Team&lt;/code&gt; 컬럼을 삽입하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  @Entity

  public class Member {
      @Id @GeneratedValue
      private Long id;

      @Column(name = &amp;quot;USERNAME&amp;quot;)
      private String name;
      private int age;

      // @Column(name = &amp;quot;TEAM_ID&amp;quot;)
      // private Long teamId;

      @ManyToOne
      @JoinColumn(name = &amp;quot;TEAM_ID&amp;quot;)
      private Team team;
      ...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  엔티티 다루기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;//팀 저장
Team team = new Team();

team.setName(&amp;quot;TeamA&amp;quot;);
em.persist(team);

//회원 저장
Member member = new Member();
member.setName(&amp;quot;member1&amp;quot;);

member.setTeam(team); //단방향 연관관계 설정, 참조 저장

em.persist(member);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;팀을 생성하고 영속성 컨텍스트에 저장한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;그리고 회원을 저장할 때, &lt;code&gt;setter&lt;/code&gt;를 이용하여 &lt;code&gt;team&lt;/code&gt;을 설정한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;이 때, 식별자를 저장하지 않고 객체 자체를 통해 set하는 모습이 차이점이다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;저장한 엔티티를 조회하자&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  //조회
  Member findMember = em.find(Member.class, member.getId());

  //참조를 사용해서 연관관계 조회
  Team findTeam = findMember.getTeam();&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;여기서 확연한 차이를 볼 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;기존에는 &lt;code&gt;findMember&lt;/code&gt;에서 조회하지 않고 다시 영속성 컨텍스트 내부를 찾아보아야 했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;em.find(Team.class, …)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;하지만 객체간의 연관관계를 구성해놨기에, 조회한 &lt;code&gt;findMember&lt;/code&gt;에서 손 쉽게 &lt;code&gt;team&lt;/code&gt;을 꺼낼 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;이렇게 객체의 참조적 연관성을 설정해주는 것을 단방향 연관관계라 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Ref&lt;/h1&gt;
&lt;p&gt;김영한 강사님, JPA 프로그래밍 - 기본편&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;

&lt;/div&gt;</description>
      <category>Dev/Spring</category>
      <category>joincolumn</category>
      <category>JPA</category>
      <category>ManyToOne</category>
      <category>onetomany</category>
      <category>SpringBoot</category>
      <category>단방향 매핑</category>
      <category>양방향 매핑</category>
      <category>연관관계 mappedby</category>
      <category>연관관계 매핑</category>
      <category>연관관계 주인</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/82</guid>
      <comments>https://oxdjww.tistory.com/82#entry82comment</comments>
      <pubDate>Tue, 21 Nov 2023 01:47:49 +0900</pubDate>
    </item>
    <item>
      <title>변경 감지(Dirty Checking)</title>
      <link>https://oxdjww.tistory.com/81</link>
      <description>&lt;div class='markdown-body'&gt;

&lt;h1&gt;✅ 영속성 컨텍스트란&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;‘엔티티를 영구히 저장하는 환경&lt;/strong&gt;’이라는 뜻&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EntityManager.persist(entity);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;하지만 이는 논리적인 개념이다.&lt;/li&gt;
&lt;li&gt;엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있다.&lt;/li&gt;
&lt;li&gt;아래의 예를 통해 영속성 컨텍스트 내에서 변경 감지(Dirty Checking)를 하는 법을 알아보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ 엔티티 수정&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;EntityManagerFactory emf = Persistence.createEntityManagerFactory(&amp;quot;hello&amp;quot;);

        // 단일 EntityManger를 생성한다.
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();

                // 트랜잭션 시작한다. (영속성 컨텍스트)
        tx.begin();

        try {
                        // memberA라는 아이디를 가진 Member 엔티티를 조회한다.
                        Member memberA = em.find(Member.class, &amp;quot;memberA&amp;quot;)

                        // 꺼내온 엔티티의 값을 수정한다.
                        memberA.setUserName(&amp;quot;hi&amp;quot;);
                        memberA.setAge(10);

            // 그리고 트랜잭션을 커밋하면 어떻게 될까?
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;커밋 전에, &lt;code&gt;em.update(member)&lt;/code&gt; 이런 코드를 통해 영속성 컨텍스트에 update를 해주어야 할 것 같다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;strong&gt;변경 감지&lt;/strong&gt; 기능이 수정사항을 확인하여 이를 처리해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ 변경 감지(Dirty Checking)&lt;/h1&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb0xNU/btsAzUjSx9I/qa2cj9kd12W7727RskOl30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb0xNU/btsAzUjSx9I/qa2cj9kd12W7727RskOl30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb0xNU/btsAzUjSx9I/qa2cj9kd12W7727RskOl30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb0xNU%2FbtsAzUjSx9I%2Fqa2cj9kd12W7727RskOl30%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tx.commit();&lt;/code&gt; 을 하게 되면, 바로 커밋이 이루어지진 않는다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flush()&lt;/code&gt; 를 통해서 영속성 컨텍스트에 수정사항이 있는지 확인하는 작업이 이루어진다.&lt;/li&gt;
&lt;li&gt;그 후, 이에 맞게 쓰기 지연 저장소에 SQL을 생성한다. (등록, 수정, 삭제 쿼리)&lt;/li&gt;
&lt;li&gt;생성된 SQL을 실행하고, 커밋한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  Flush&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;em.flush()&lt;/code&gt;  &lt;strong&gt;:&lt;/strong&gt; 직접 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;트랜잭션 커밋&lt;/code&gt; &lt;strong&gt;:&lt;/strong&gt; 플러시 자동 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JPQL 쿼리 실행&lt;/code&gt; &lt;strong&gt;:&lt;/strong&gt; 플러시 자동 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;  JPQL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JPQL 실행 시 왜 플러시가 자동 호출될까?&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;em.persist(memberA);
em.persist(memberB);
em.persist(memberC);//중간에 JPQL 실행
query = em.createQuery(&amp;quot;select m from Member m&amp;quot;, Member.class);
List&amp;lt;Member&amp;gt; members= query.getResultList();&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;중간에 저런 쿼리가 실행되는 상황이라면 어떨까&lt;/li&gt;
&lt;li&gt;분명히 &lt;code&gt;memberA&lt;/code&gt;, &lt;code&gt;memberB&lt;/code&gt;, &lt;code&gt;memberC&lt;/code&gt;를 넣었는데, 조회되지 않는 상황이 나온다.&lt;/li&gt;
&lt;li&gt;JPQL을 실행하여 쿼리문을 직접 실행하기 직전에 내부적으로 &lt;code&gt;flush()&lt;/code&gt;를 통해 이런 상황을 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;  Flush Options&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;em.setFlushMode(FlushModeType.COMMIT)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FlushModeType.AUTO&lt;/code&gt; :  커밋이나 쿼리를 실행할 때 플러시 (기본값)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FlushModeType.COMMIT&lt;/code&gt; : 커밋할 때만 플러시&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Ref&lt;/h1&gt;
&lt;p&gt;김영한 강사님, JPA 프로그래밍 - 기본편&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;

&lt;/div&gt;</description>
      <category>Dev/Spring</category>
      <category>dirty checking</category>
      <category>em.persist</category>
      <category>EntityManager</category>
      <category>flush</category>
      <category>JPA</category>
      <category>jpa 영속성 컨텍스트</category>
      <category>persist</category>
      <category>SQL</category>
      <category>더티체킹</category>
      <category>변경감지</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/81</guid>
      <comments>https://oxdjww.tistory.com/81#entry81comment</comments>
      <pubDate>Mon, 20 Nov 2023 23:43:21 +0900</pubDate>
    </item>
    <item>
      <title>JPA를 왜 쓸까?</title>
      <link>https://oxdjww.tistory.com/80</link>
      <description>&lt;div class=&quot;markdown-body&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;✅ JPA?&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Java Persistence API&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;자바 진영의 ORM 기술의 표준
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORM이란?즉, 객체 관계 매핑이다.&lt;b&gt;ORM&lt;/b&gt;이라는 프레임워크가 중간에서 매핑하는 것&lt;/li&gt;
&lt;li&gt;대중적인 언어에서는 대부분 &lt;b&gt;ORM&lt;/b&gt;기술이 존재하여, 데이터베이스와 어플리케이션 간의 인터페이스를 보장한다. (ex. &lt;code&gt;python sqlalchemy&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;객체는 객체대로 설계 / 관계형 데이터베이스는 관계형 데이터베이스대로 설계&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Object-Relational Mapping&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ JPA의 동작&lt;/h1&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfDo0E/btsAyroYPvm/8csEiQyiyhhfMkJUknNyNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfDo0E/btsAyroYPvm/8csEiQyiyhhfMkJUknNyNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfDo0E/btsAyroYPvm/8csEiQyiyhhfMkJUknNyNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfDo0E%2FbtsAyroYPvm%2F8csEiQyiyhhfMkJUknNyNK%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;520&quot; height=&quot;253&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어플리케이션과 JDBC 사이에서 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ 표준 명세&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA는 기본적으로 인터페이스의 모음이다.&lt;/li&gt;
&lt;li&gt;이 JPA 표준 명세를 구현한 3가지 구현체가 존재한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Hibernate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EclipseLink&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DataNucleus&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;✅ JPA 사용 이유?&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL 중심적인 개발에서 객체 중심으로 개발이 가능해진다.&lt;/li&gt;
&lt;li&gt;생산성&lt;/li&gt;
&lt;li&gt;유지보수&lt;/li&gt;
&lt;li&gt;패러다임 불일치 해결
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL 방언 처리 기능 등으로 패러다임의 불일치를 해결한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;성능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1차 캐시 등 다양한 이유로 성능상의 이점을 제공한다.&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;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;b&gt;jpa.persist&lt;/b&gt;(member)&lt;/li&gt;
&lt;li&gt;조회: Member member = &lt;b&gt;jpa.find&lt;/b&gt;(memberId)&lt;/li&gt;
&lt;li&gt;수정: &lt;b&gt;member.setName&lt;/b&gt;(&amp;ldquo;변경할 이름&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;삭제: &lt;b&gt;jpa.remove&lt;/b&gt;(member)&lt;/li&gt;
&lt;li&gt;JPA의 간단한 표현으로 &lt;b&gt;CRUD&lt;/b&gt;를 손쉽게 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  유지보수&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zeFGo/btsAGRsCQ3G/9gXx6nrAiRoZXCY77x2UTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zeFGo/btsAGRsCQ3G/9gXx6nrAiRoZXCY77x2UTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zeFGo/btsAGRsCQ3G/9gXx6nrAiRoZXCY77x2UTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzeFGo%2FbtsAGRsCQ3G%2F9gXx6nrAiRoZXCY77x2UTk%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;698&quot; height=&quot;329&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA 필드만 추가하면 SQL은 JPA가 알아서 처리해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  패러다임 불일치 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  JPA와 상속&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r30D4/btsAFZRYB2e/ACEq5IjzjRFYTLpnDdc8Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r30D4/btsAFZRYB2e/ACEq5IjzjRFYTLpnDdc8Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r30D4/btsAFZRYB2e/ACEq5IjzjRFYTLpnDdc8Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr30D4%2FbtsAFZRYB2e%2FACEq5IjzjRFYTLpnDdc8Nk%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;704&quot; height=&quot;265&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&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;개발자가 할 일은 &lt;code&gt;jpa.persist(album);&lt;/code&gt; , &lt;code&gt;jpa.find(Album.class, albumId);&lt;/code&gt;뿐이다.&lt;/li&gt;
&lt;li&gt;나머지는 JPA가 처리해준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;INSERT INTO ITEM ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT INTO ALBUM ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;or&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELECT I.*, A.* FROM ITEM I JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  JPA와 연관관계&lt;/h3&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;member.setTeam(team);
jpa.persist(member);&lt;/code&gt;&lt;/pre&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  JPA와 객체 그래프 탐색&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Member member = jpa.find(Member.class, memberId)
Team team = member.getTeam();&lt;/code&gt;&lt;/pre&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  JPA와 비교하기&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;String memberId = &quot;100&quot;;
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

member1 == member2; //같다.&lt;/code&gt;&lt;/pre&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;/ul&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;1차 캐시와 동일성(identity) 보장&lt;/li&gt;
&lt;li&gt;트랜잭션을 지원하는 쓰기 지연(transactional write-behind)&lt;/li&gt;
&lt;li&gt;지연 로딩(Lazy Loading)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  1차 캐시와 동일성(identity) 보장&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DB Isolation Level이 Read Commit이어도 어플리케이션에서 Repeatable Read 보장&lt;/li&gt;
&lt;li&gt;JPA는 영속성 컨텍스트 내에 1차 캐시를 보유한다.&lt;/li&gt;
&lt;li&gt;한 트랜잭션 내에서 처음으로 조회가 들어오는 엔티티에 대해, 1차 캐시에 존재하면 이를 반환한다.&lt;/li&gt;
&lt;li&gt;아니라면 데이터베이스에서 꺼내오고, (SQL 실행) 이를 1차 캐시에 저장하고 이를 반환한다.&lt;/li&gt;
&lt;li&gt;즉, 아래와 같은 구문에서 SQL은 단 한번만 실행된다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String memberId = &quot;100&quot;; Member m1 = jpa.find(Member.class, memberId); //SQL Member m2 = jpa.find(Member.class, memberId); //캐시 println(m1 == m2) //true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  트랜잭션을 지원하는 쓰기 지연(transactional write-behind)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션을 커밋 (&lt;code&gt;tx.commit()&lt;/code&gt;) 할 때 까지 &lt;code&gt;INSERT SQL&lt;/code&gt; 을 모은다.&lt;/li&gt;
&lt;li&gt;JDBC의 BATCH SQL기능을 사용하여 한번에 SQL을 전송한다.&lt;/li&gt;
&lt;li&gt;즉, &lt;code&gt;em.persiste(entity)&lt;/code&gt; 를 통해 영속성 컨텍스트에 저장한다고 해서 바로 SQL을 실행하는 것이 아니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;em.persist(memberA); em.persist(memberB); em.persist(memberC);//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다. //커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다. transaction.commit(); // [트랜잭션] 커밋&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UPDATE, DELETE SQL&lt;/code&gt; 도 마찬가지이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transaction.begin(); // [트랜잭션] 시작 changeMember(memberA); deleteMember(memberB); 비즈니스_로직_수행(); //비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않는다. //커밋하는 순간 데이터베이스에 UPDATE, DELETE SQL을 보낸다. transaction.commit(); // [트랜잭션] 커밋&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  지연 로딩(Lazy Loading)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지연 로딩 : 객체가 실제로 사용될 때 Database에서 로딩한다.&lt;/li&gt;
&lt;li&gt;즉시 로딩 : JOIN SQL을 통해 연관된 객체를 미리 조회한다.&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자는 SQL 쿼리를 직접 작성하는 대신, JPA가 제공하는 메서드를 사용하여 데이터를 조작한다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 데이터베이스를 사용하든, JPA는 데이터베이스의 구현체에 상관없이 이를 번역하여 개발자가 특정 데이터베이스에 종속되지 않도록 한다.&lt;/li&gt;
&lt;/ul&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;JPA는 표준이다!&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Ref&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김영한 강사님, JPA 프로그래밍 - 기본편&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Dev/Spring</category>
      <author>oxdjww</author>
      <guid isPermaLink="true">https://oxdjww.tistory.com/80</guid>
      <comments>https://oxdjww.tistory.com/80#entry80comment</comments>
      <pubDate>Mon, 20 Nov 2023 23:40:03 +0900</pubDate>
    </item>
  </channel>
</rss>