<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zwon.log</title>
        <link>https://velog.io/</link>
        <description>Backend 관련 지식을 정리하는 Back과사전</description>
        <lastBuildDate>Mon, 11 May 2026 13:15:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>zwon.log</title>
            <url>https://velog.velcdn.com/images/security-won/profile/417d8e91-7c80-44f6-bcc7-94975d644978/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. zwon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/security-won" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[코드트리] 청약 통장 1회차 미션 - 코딩테스트 준비, 갭체크부터 시작하기]]></title>
            <link>https://velog.io/@security-won/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%B2%AD%EC%95%BD-%ED%86%B5%EC%9E%A5-1%ED%9A%8C%EC%B0%A8-%EB%AF%B8%EC%85%98-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A4%80%EB%B9%84-%EA%B0%AD%EC%B2%B4%ED%81%AC%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@security-won/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%B2%AD%EC%95%BD-%ED%86%B5%EC%9E%A5-1%ED%9A%8C%EC%B0%A8-%EB%AF%B8%EC%85%98-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A4%80%EB%B9%84-%EA%B0%AD%EC%B2%B4%ED%81%AC%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 11 May 2026 13:15:03 GMT</pubDate>
            <description><![CDATA[<p>평소 코딩테스트 공부를 해야겠다고 생각만 하고 제대로 실천하지 못하고 있었다.
뭔가 알고리즘은 항상 뒤로 미루게 되었고, 막상 문제를 풀려고 하면 손이 잘 가지 않았다.</p>
<p>그러던 중 우연히 코드트리 청약통장 챌린지를 발견하게 되었다.</p>
<p>평소에도 코드트리는 많이 들어봤지만 유료 플랫폼이라는 인식이 있어서 무료 사이트 위주로만 공부했었다. 그런데 이번 챌린지는 완주 시 일정 기간 무료 이용 혜택도 제공하고 있어서 이번 기회에 코딩테스트 공부 습관을 만들어보자라는 생각으로 신청하게 되었다.</p>
<p>특히 7주 동안 꾸준히 인증하면 8월 31일까지 무료 연장이 가능하다는 점이 동기부여가 되었다. 단순히 문제 몇 개 푸는 이벤트가 아니라 꾸준히 공부하는 루틴을 만드는 챌린지라는 점이 좋았다.</p>
<p>코드트리 청약 통장 사이트는 다음과 같다.</p>
<p><a href="https://www.codetree.ai/ko/no-free-lunch-2026/?ref=LV93D6">https://www.codetree.ai/ko/no-free-lunch-2026/?ref=LV93D6</a></p>
<p>나의 갭체크 결과는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/security-won/post/9f184c48-a4bc-46ce-90ca-00e391a0e86d/image.png" alt=""></p>
<p>사실 어느 정도는 부족할 거라고 생각했지만, 예상보다 훨씬 부족한 결과가 나와서 꽤 충격을 받았다.</p>
<p>시간 안에 못 푼 문제도 있었지만, 개인적으로 더 충격이었던 건 생각보다 기본적인 문제도 제대로 못 풀었다는 점이었다.평소에 구현 정도는 할 수 있다고 생각했는데 실제 제한 시간 안에서 문제를 읽고 로직을 설계하고 예외를 처리하는 과정은 완전히 다른 영역이었다.</p>
<p>특히 문제를 보자마자 바로 풀이 방향이 떠오르지 않는 경우가 많았고 알꺼같은데.. 알꺼같은데 하면서 구현하면서 시간을 까먹기도했다.</p>
<p>이런 갭체크를 통해서 얻어간 것도 있다.
문제를 풀면서 어떤 부분이 부족한지 조금 더 구체적으로 알게 되었다는 점이다.
그리고 갭체크 이후 부족한 부분을 채울 수 있도록 챕터를 추천해준다. 단순히 점수만 보여주는 것이 아니라 어떤 유형을 먼저 공부해야 하는지 방향성을 알려줘서 좋았다.특히 혼자 공부하면 무엇부터 해야 할지 막막한 경우가 많은데, 단계별로 문제를 풀 수 있도록 구성되어 있어서 코딩테스트를 연습하는 사람 입장에서 도움이 될 것 같았다.</p>
<p>아직 첫 주차라 실력이 크게 늘었다고 말하기는 어렵지만,
적어도 이번 챌린지를 통해 “매일 조금이라도 문제를 푸는 습관”은 만들어보고 싶다.</p>
<p>당장 어려운 문제를 푸는 것보다도 꾸준히 문제를 읽고 고민하는 시간을 늘리는 게 현재 내 목표다.</p>
<p>#코드트리 #코딩테스트 #코테공부 #코테준비 #알고리즘공부 #갭체크</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LeetCode - 45. Jump Game II]]></title>
            <link>https://velog.io/@security-won/LeetCode-45.-Jump-Game-II</link>
            <guid>https://velog.io/@security-won/LeetCode-45.-Jump-Game-II</guid>
            <pubDate>Sat, 25 Nov 2023 03:58:59 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>문제 : <a href="https://leetcode.com/problems/jump-game-ii/">45. Jump Game II</a>
문제는 nums[0]에서 j만큼 점프할 수 있고( nums[i + j] ) 최소한의 점프로 nums[i-1]에 도달하는 경우의 수를 구한 후 점프 횟수를 반환하는 것이다.
조건은 다음과 같다.</p>
<ul>
<li>1) 0 &lt;= j &lt;= nums[i]</li>
<li>2) i + j &lt; n<h2 id="풀이">풀이</h2>
<pre><code class="language-python">class Solution:
  def jump(self, nums: List[int]) -&gt; int:
      jumpCnt = [0] * len(nums)
      for i in range(len(nums)):
          for j in range(1, nums[i]+1): # 0 &lt;= j &lt;= nums[i]
              if i + j &gt;= len(nums):
                  break
              if jumpCnt[i + j] == 0 or jumpCnt[i+j] &gt; jumpCnt[i] + 1:
                  jumpCnt[i + j] = jumpCnt[i] + 1
      return jumpCnt[-1]</code></pre>
</li>
<li>for j in range(1, nums[i]+1)에서 0부터 시작하지 않은 이유는 0은 제자리 점프기 때문에 1부터 시작하도록 했다.</li>
<li>내가 생각하는 이 코드의 핵심이다.<pre><code class="language-python">if jumpCnt[i + j] == 0 or jumpCnt[i+j] &gt; jumpCnt[i] + 1:
   jumpCnt[i + j] = jumpCnt[i] + 1</code></pre>
</li>
<li>현재위치 i에서 점프할 위치인 i + j의 값이 0이면 jumpCnt[i+j]는 점프할려는 위치의 값 + 1을 해주면 된다.</li>
<li>각 배열에는 해당 위치까지 오는데 필요한 점프 횟수가 jumpCnt 배열에 저장되어있기때문에 jumpCnt[i] + 1의 값을 jumpCnt[i + j]에 할당해주는 것이다.</li>
<li>그리고 점프할 위치인 jumpCnt[i+j]값이 0이아닌경우에는 jumpCnt[i+j]값보다 jumpCnt[i] + 1의 값이 작은 경우엔 junpCnt[i+j]값을 바꿔준다.<h2 id="메모">메모</h2>
<ul>
<li>DP 사용</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LeetCode - 150. Evaluate Reverse Polish Notation
]]></title>
            <link>https://velog.io/@security-won/LeetConde-61.-Rotate-List-jx5pgqi0</link>
            <guid>https://velog.io/@security-won/LeetConde-61.-Rotate-List-jx5pgqi0</guid>
            <pubDate>Thu, 23 Nov 2023 17:55:22 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>문제는 간단하다. 
후위연산자 계산이다.
문제 링크 : <a href="https://leetcode.com/problems/evaluate-reverse-polish-notation/?envType=study-plan-v2&amp;envId=top-interview-150">LeetCode - 150. Evaluate Reverse Polish Notation</a></p>
<h2 id="풀이">풀이</h2>
<p>stack을 사용했다. </p>
<ul>
<li>연산자가 나올때까지 숫자들을 stack에 append한다.</li>
<li>피연산자가 나오면 stack[-1]과 stack[-2]의 숫자를 연산한다.<ul>
<li>순서는 ex) <code>stack[-2] + stack[-1]</code></li>
</ul>
</li>
</ul>
<p>후위연산자 표기법에 대해서는 <a href="https://wikidocs.net/192124">여기</a>를 확인하자.</p>
<pre><code class="language-python">class Solution:
    def evalRPN(self, tokens: List[str]) -&gt; int:
        stack = []
        operator = [&#39;+&#39;, &#39;-&#39;, &quot;/&quot;, &quot;*&quot;]
        for token in tokens:
            if token not in operator: # number
                stack.append(token)
            else:
                num1 = int(stack.pop())
                num2 = int(stack.pop())
                if token == &#39;+&#39;:
                    stack.append(num2 + num1)
                elif token == &#39;-&#39;:
                    stack.append(num2 - num1)
                elif token == &#39;*&#39;:
                    stack.append(num2 * num1)
                elif token == &#39;/&#39;:
                    stack.append(num2 / num1)
        return int(stack.pop())</code></pre>
<h2 id="메모">메모</h2>
<ul>
<li>stack 사용</li>
<li>후위연산자 </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LeetConde - 61. Rotate List
]]></title>
            <link>https://velog.io/@security-won/LeetConde-61.-Rotate-List</link>
            <guid>https://velog.io/@security-won/LeetConde-61.-Rotate-List</guid>
            <pubDate>Thu, 23 Nov 2023 13:50:46 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>문제 : <a href="https://leetcode.com/problems/rotate-list/description/?envType=study-plan-v2&amp;envId=top-interview-150">61. Rotate List</a></p>
<p>문제는 간단하다.
linked list가 주어지고 k만큼 오른쪽으로 회전한 결과값을 반환하면 된다.</p>
<img src="https://velog.velcdn.com/images/security-won/post/bb8cf668-6fe1-4c88-bf0e-8bf32b883007/image.png" width=600px>

<h2 id="풀이">풀이</h2>
<pre><code class="language-python">from collections import deque
class Solution(object):
    def rotateRight(self, head, k):
        if head == None:
            return None

        answer = deque()
        curr = head

        while curr:
            if curr.next == None:
                answer.append(curr.val)
                break
            answer.append(curr.val)
            curr = curr.next

        answer.rotate(k)

        listNode = ListNode(answer[0])
        curr_node = listNode
        for i in range(1,len(answer)):
            new_node = ListNode(answer[i])
            curr_node.next = new_node
            curr_node = curr_node.next
        return listNode</code></pre>
<ul>
<li>간단하게 설명하자면 while문을 돌아서 deque()에 val값들을 넣어주고, 반환타입이 listNode라서 deque()의 값들을 하나씩 꺼내 ListNode로 생성해주고 연결해주었다.</li>
<li>처음엔 curr_node가 아닌 listNode를 바로 사용해서 작성했더니,<pre><code class="language-python">listNode = ListNode(answer[0])
  for i in range(1,len(answer)):
      new_node = ListNode(answer[i])
      listNode.next = new_node
      listNode = listNode.next
  return listNode</code></pre>
</li>
<li>listNode = listNode.next때문에 항상 마지막 값으로 세팅되게 해줘서 마지막 노드만 반환되었다.</li>
<li>그래서 curr_node를 생성해서 연결한 후 반환값으로 첫 번째 노드를 반환하도록 해주었다.<h2 id="메모">메모</h2>
</li>
<li>deque()의 rotate()함수 사용</li>
<li>deque()의 rotate()함수를 사용하지 않고 풀어봐야겠다.</li>
<li>linked List 복습</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 - 사용자 프로필 설정 및 변경]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%94%84%EB%A1%9C%ED%95%84-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%94%84%EB%A1%9C%ED%95%84-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Fri, 27 Oct 2023 21:53:46 GMT</pubDate>
            <description><![CDATA[<p>사용자 프로필 설정 기능이다.
롬복 어노테이션들은 생략하고 포스팅하고자 한다.</p>
<h2 id="user">User</h2>
<pre><code class="language-java">public class User extends BaseTimeEntity {
  ...
  @OneToOne(mappedBy = &quot;user&quot;, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  private Profile profile;

  public void updateProfile(Profile newProfile) {
    this.profile = newProfile;
    newProfile.setUser(this);
  }
  ...</code></pre>
<ul>
<li>회원 탈퇴 시 프로필 사진도 삭제되게 CascadeType.ALL로 설정하였다.</li>
</ul>
<h2 id="profile">Profile</h2>
<pre><code class="language-java">public class Profile {

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

  private String originFilename; // 업로드된 파일 이름
  private String storeFilename; // 저장한 파일명 ex) 132-5sdf-23451-1as.png -&gt; UUID로 식별자 생성 + 확장자

  @OneToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = &quot;user_id&quot;)
  private User user;

  public void setUser(User user) {
    this.user = user;
  }
}</code></pre>
<h2 id="imgcontroller">ImgController</h2>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
public class ImgController {
  private final FileStore fileStore;
  private final FileService fileService;


  @PostMapping(&quot;/profile/update&quot;) // 프로필 사진 변경과 삭제
  public String profileUpdate(@ModelAttribute ProfileUpdateDto profileUpdateDto,
                              HttpSession session) throws IOException {
    User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);

    if (profileUpdateDto.getProfile().isEmpty()) { 
      fileService.deleteBeforeProfile(loginUser.getNickname());
    } else {
      fileService.transferProfile(profileUpdateDto.getProfile(), loginUser.getNickname());
    }

    return &quot;redirect:/my-page&quot;;
  }
  // 프로필 이미지 보여주기
  @ResponseBody
  @GetMapping(&quot;/profile/{filename}&quot;)
  public Resource downloadProfile(@PathVariable String filename)
      throws MalformedURLException {
    return new UrlResource(&quot;file:&quot; + fileStore.getProfileFullPath(filename));
  }
}</code></pre>
<h2 id="filestore">FileStore</h2>
<ul>
<li><p>첨부파일 업로드 포스팅과 별 차이 없다.</p>
</li>
<li><p>추가된 부분만 정리하겠다.</p>
<pre><code class="language-java">// 프로필 이미지
@Value(&quot;${profile.dir}&quot;)
private String profileDir;

// 파일 저장 경로 Full
public String getProfileFullPath(String filename){
  return profileDir + filename;
}

public Profile profileImgStore(MultipartFile multipartFile) throws IOException {
  if (multipartFile.isEmpty()){
    return null;
  }
  String originalFileName = multipartFile.getOriginalFilename();
  String ext = extractedExt(originalFileName);
  String storeFileName = createStoreFileName(ext);
  multipartFile.transferTo(new File(getProfileFullPath(storeFileName)));

  return Profile.builder()
      .originFilename(originalFileName)
      .storeFilename(storeFileName)
      .build();
}</code></pre>
</li>
<li><p>첨부파일 구현과 똑같고 약간 다른거는 파일 경로 가져오는거나(따로 폴더를 생성함) ,, 말고는 없다.</p>
<h2 id="fileservice">FileService</h2>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
public class FileService {
private final FileStore fileStore;
private final FileRepository fileRepository; 
private final UserRepository userRepository;
private final ProfileRepository profileRepository;

</code></pre>
</li>
</ul>
<p>  // 프로필 이미지 변경
  @Transactional
  public Profile transferProfile(MultipartFile profile, String nickname) throws IOException {
    User user = userRepository.findUserByNickname(nickname).orElseThrow(IllegalArgumentException::new);
    Profile profileImg = fileStore.profileImgStore(profile);
    user.updateProfile(profileImg);
    return profileImg;
  }</p>
<p>  public void deleteBeforeProfile(String nickname) {
    User user = userRepository.findUserByNickname(nickname).orElseThrow(IllegalArgumentException::new);</p>
<pre><code>if (user.getProfile() != null) {
  Profile profile = user.getProfile();

  File imgFileOnDisk = new File(fileStore.getProfileFullPath(profile.getStoreFilename()));
  if (imgFileOnDisk.exists()) {
    imgFileOnDisk.delete();
  }
  user.updateProfile(null);
  profileRepository.delete(profile);
}</code></pre><p>  }</p>
<p>}</p>
<pre><code>- 이 부분도 첨부파일과는 별 차이 없다.

## 마이페이지
```html
&lt;img id=&quot;profile&quot; th:src=&quot;|/profile/profile.png|&quot; alt=&quot;프로필 사진&quot; th:if=&quot;${loginUser.profile == null}&quot;&gt;
&lt;img th:src=&quot;|/profile/${loginUser.profile.getStoreFilename()}|&quot; th:if=&quot;${loginUser.profile != null}&quot;/&gt;</code></pre><ul>
<li>기본 이미지 같은 경우는 프로필을 저장하는 로컬 폴더에 저장해두고 사용자의 profile에 사용자가 저장판 프로필 사진이 없으면 기본 이미지가 보이도록 구현하였다ㅏ.</li>
</ul>
<h2 id="화면">화면</h2>
<p><img src="https://velog.velcdn.com/images/security-won/post/19f873ef-9327-473c-a754-738aa6432774/image.png" alt=""></p>
<ul>
<li>프로필을 설정하지 않은 경우</li>
</ul>
<p><img src="https://velog.velcdn.com/images/security-won/post/e98977af-9c66-4637-9905-fecdb4acfac4/image.png" alt=""></p>
<ul>
<li>프로필을 설정한 경우</li>
<li>프로필 수정을 누르면 수정폼이 나오도록 자바스크립트 코드를 작성했다. (코드 생략)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/security-won/post/7cdea2f6-1256-4bfa-8519-09780d8e754d/image.png" alt=""></p>
<ul>
<li>프로필 설정을 하면 다음과 같이 크루 상세 조회에서 크루원 정보를 나타낼 때 프로필 사진도 나오도록 했다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/security-won/post/aaf754bf-3626-4f46-b313-9f4d60624265/image.png" alt=""></p>
<ul>
<li>프로필 설정 안한 경우</li>
</ul>
<p><img src="https://velog.velcdn.com/images/security-won/post/f1acaa8c-f206-41bd-be45-334deeb5e057/image.png" alt=""></p>
<p>크루의 Banner이미지도 사용자 프로필 설정하는 코드와 거의 매서드명이랑 반환타입만 다르고 동일한 방식으로 구현해서 크루의 배너 설정의 포스팅은 생략하겠다.</p>
<hr>
<h2 id="깨달음">깨달음..</h2>
<p>객체가 영속성 컨텍스트에 있는줄 알았는데 없어서 원하는대로 결과가 나오지 않는 일을 많이 겪으면서 영속성 컨텍스트의 동작과정을 잘 이해해야함을 너무나 깨달았다.</p>
<p>자꾸 session에서 로그인한 user를 가져오면서 사용하고있는데, 영속성 컨텍스트에 올라가있는줄 착각하고 개발을 하다보다 잘못된 방식으로 작성한 코드가 많으거같아서 하나씩 수정하고자한다.</p>
<p>나중에 OAuth로 로그인 구현 후 전체적인 리펙토링이 필요할꺼라고 생각하고있는데 계속 기능 개발하면서 수정해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 마이페이지 구현 (1)]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EB%A7%88%EC%9D%B4%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EB%A7%88%EC%9D%B4%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Fri, 27 Oct 2023 10:37:26 GMT</pubDate>
            <description><![CDATA[<p>먼저 사용자가 관심있어요 누른 크루즈와 사용자가 참여중인 크루즈를 가져오는 코드이다.
그전에 User와 Crew 엔티티 연관관계를 단방향이었다가 양방향으로 다음과 같이 설정했다.
코드를 구현하면서 느낀게 스트림을 정말 잘 다루면 너무 좋을꺼같다.........</p>
<h2 id="user">User</h2>
<pre><code class="language-java">public class User extends BaseTimeEntity {
    ...
    @OneToMany(mappedBy = &quot;user&quot;)
      private List&lt;Crew&gt; leaderCrews = new ArrayList&lt;&gt;(); // 내가 크루장인 크루

     @ManyToMany(mappedBy = &quot;crew&quot;)
      private List&lt;Crew&gt; crews = new ArrayList&lt;&gt;(); // 내가 참여중인 크루
    ...</code></pre>
<h2 id="crew">Crew</h2>
<pre><code class="language-java">public class Crew extends BaseTimeEntity {
    ...
     @ManyToMany// 크루에 속한 팀원, 우선 ManyToMany로 함.
      @JoinTable(
      name = &quot;user_crew&quot;,
      joinColumns = @JoinColumn(name = &quot;crew_id&quot;),
      inverseJoinColumns = @JoinColumn(name = &quot;user_id&quot;)
  )
  private List&lt;User&gt; users = new ArrayList&lt;&gt;();
    ...</code></pre>
<h2 id="userservice">UserService</h2>
<pre><code class="language-java">// 관심 있는 크루
  public List&lt;CrewResponseDto&gt; LikeCrew(Long id){
    User user = userRepository.findById(id)
    .orElseThrow(() -&gt; new blogException(&quot;해당 회원은 존재하지 않습니다.&quot;));

    List&lt;CrewResponseDto&gt; crews = user.getLikes().stream()
        .map(like -&gt; new CrewResponseDto(like.getCrew()))
        .collect(Collectors.toList());
    return crews;
  }
  // 참여하고 있는 크루
  public List&lt;CrewResponseDto&gt; joinCrew(Long id){
    User user = userRepository.findById(id)
    .orElseThrow(() -&gt; new blogException(&quot;해당 회원은 존재하지 않습니다.&quot;));

    List&lt;CrewResponseDto&gt; crews = user.getCrews().stream()
        .map(CrewResponseDto::new)
        .collect(Collectors.toList());
    return crews;
  }

  // 크루장인 크루
  public List&lt;CrewResponseDto&gt; leaderCrew(String nickname){
    User user = userRepository.findUserByNickname(nickname)
        .orElseThrow(() -&gt; new blogException(&quot;해당 회원은 존재하지 않습니다.&quot;));

    List&lt;CrewResponseDto&gt; crews = user.getLeaderCrews().stream()
        .filter(crew -&gt; crew.getUser().equals(user))
        .map(CrewResponseDto::new)
        .collect(Collectors.toList());
    return crews;
  }

  // 활동종료 크루 (크루원일 때)
  public List&lt;CrewResponseDto&gt; closeCrew(String nickname){
    User user = userRepository.findUserByNickname(nickname)
        .orElseThrow(() -&gt; new blogException(&quot;해당 회원은 존재하지 않습니다.&quot;));

    List&lt;CrewResponseDto&gt; crews = user.getCrews().stream()
        .filter(crew -&gt; crew.isClosed())
        .map(CrewResponseDto::new)
        .collect(Collectors.toList());
    return crews;
}</code></pre>
<ul>
<li>참고로 new blogException는 사용자 정의 예외를 한번 해본건데 테스트(?)느낌이라 아마 나중에 예외 처리를 따로 할 때 수정할 예정이다.</li>
</ul>
<h2 id="usercontroller">UserController</h2>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
public class UserController {

  private final UserService userService;

  @GetMapping(&quot;/my-page&quot;)
  public String myPage(Model model, HttpSession session){
    User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
    // 크루장인 크루즈
    List&lt;CrewResponseDto&gt; leaderCrew = userService.leaderCrew(loginUser.getNickname());
    model.addAttribute(&quot;leaderCrew&quot;, leaderCrew);

    // &quot;관심있어요&quot; 크루즈
    List&lt;CrewResponseDto&gt; likeCrews = userService.LikeCrew(loginUser.getNickname());
    model.addAttribute(&quot;likeCrews&quot;, likeCrews);

    // 내가 참여한 크루즈
    List&lt;CrewResponseDto&gt; joinCrews = userService.joinCrew(loginUser.getNickname());
    model.addAttribute(&quot;joinCrews&quot;, joinCrews);

    return &quot;/user/my-page&quot;;
  }
}</code></pre>
<h2 id="테스트-화면">테스트 화면</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;마이 페이지&lt;/title&gt;
  &lt;link rel=&quot;stylesheet&quot; th:href=&quot;@{/css/mypage.css}&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h3&gt;관리 중인 크루 (크루장인 경우)&lt;/h3&gt;
  &lt;div th:each=&quot;crew : ${leaderCrew}&quot;&gt;
    &lt;p th:text=&quot;${crew.name}&quot;&gt;&lt;/p&gt;
  &lt;/div&gt;
&lt;h3&gt;관심 있는 크루&lt;/h3&gt;
  &lt;div th:each=&quot;crew : ${likeCrews}&quot;&gt;
    &lt;p th:text=&quot;${crew.name}&quot;&gt;&lt;/p&gt;
  &lt;/div&gt;
&lt;h3&gt;참여하고 있는 크루&lt;/h3&gt;
  &lt;div th:each=&quot;crew : ${joinCrews}&quot;&gt;
    &lt;p th:if=&quot;${crew.isClosed()}&quot;&gt;활동 종료&lt;/p&gt;
    &lt;p th:text=&quot;${crew.name}&quot;&gt;&lt;/p&gt;
  &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/security-won/post/0f514354-e694-4cff-bf18-d2cf86ddd565/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 서비스 - 크루 참가 및 탈퇴 + 크루 세팅]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%81%AC%EB%A3%A8-%EC%B0%B8%EA%B0%80-%EB%B0%8F-%ED%83%88%ED%87%B4-%ED%81%AC%EB%A3%A8-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%81%AC%EB%A3%A8-%EC%B0%B8%EA%B0%80-%EB%B0%8F-%ED%83%88%ED%87%B4-%ED%81%AC%EB%A3%A8-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Thu, 26 Oct 2023 07:27:28 GMT</pubDate>
            <description><![CDATA[<p>크루에 참여 및 탈퇴 기능과 크루 세팅하는 부분에 대해서 정리하고자 한다.
크루 세팅은 크루 공개/비공개, 크루원 모집 혹은 모집X인지, 크루 활동이 종료되었는지 아닌지를 설정하는 코드를 정리하고자 한다.</p>
<h2 id="crew">Crew</h2>
<pre><code class="language-java">@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
public class Crew extends BaseTimeEntity {

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

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = &quot;user_id&quot;)
  private User user; // 크루장 - 로그인한 user

  private String name; // 크루이름
  private boolean type; //  true-온라인, false-오프라인
  private boolean cost; // true-유료탑승, false-무료탑승
  private boolean isRecruiting; // true - 모집중, false - 모집X,
  private boolean isPublished; // true - 공개O, false - 공개X
  private boolean isClosed; // true - 종료
  private String thumbnail; //이미지 경로 , 처음엔 기본 이미지 -&gt; 크루 설정을 통해 배너 수정 가능

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String description; // 항해 목적

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String wisher; // 원하는 선원

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String plan; // 크루즈 설명


  @ManyToMany // 크루에 속한 팀원
  @JoinTable(name = &quot;user_crew&quot;)
  private List&lt;User&gt; users = new ArrayList&lt;&gt;();

  @OneToMany(mappedBy = &quot;crew&quot;)
  private List&lt;Like&gt; likes = new ArrayList&lt;&gt;();

  // 크루 가입
  public void addUser(User user){
    if(!this.users.contains(user)) {
      this.users.add(user);
    }
  }
  // 크루 탈퇴
  public void removeUser(User user) {
    if (this.users.contains(user)) {
      this.users.remove(user);
    }
  }

  // 크루에 가입이 가능한지
  public boolean isJoinable(User user) { // (1)
    return this.isPublished &amp;&amp; this.isRecruiting &amp;&amp; !this.users.contains(user) &amp;&amp; !this.isClosed;
  }
  // 크루 멤버인지
  public boolean isMember(User user) { // (2)
    if (this.user.equals(user)){ // 크루장인 경우
      return true;
    }
    return this.users.contains(user); // 크루원인 경우
  }

  // 크루장
  public void setUser(User user){
    this.user = user;
  }

  public void update(CrewUpdateRequestDto crewUpdateRequestDto){
    this.name = crewUpdateRequestDto.getName();
    this.type = crewUpdateRequestDto.isType();
    this.cost = crewUpdateRequestDto.isCost();
    this.description = crewUpdateRequestDto.getDescription();
    this.wisher = crewUpdateRequestDto.getWisher();
    this.plan = crewUpdateRequestDto.getPlan();
  }

  public void setPublished(){
    if (this.isPublished) {
      this.isPublished = false;
    } else {
      this.isPublished = true;
    }

  }
  public void setRecruited(){
    if (this.isRecruiting) {
      this.isRecruiting = false;
    } else {
      this.isRecruiting = true;
    }
  }
  public void setClosed(){
    if (this.isClosed) {
      this.isClosed = false;
    } else {
      this.isClosed = true;
    }
  }

}
</code></pre>
<ul>
<li>코드를 보면 이해가 갈 것이다.</li>
<li>그냥 단순히 true/false 또는 contains(...)를 이용해서 포함하고있는지 아닌지를 확인하는 코드들이라 설명이 필요해 보이지는 않는다. </li>
<li>주석으로도 설명을 작성해놨다. <h2 id="dto">Dto</h2>
<h3 id="crewsaverequestdto">CrewSaveRequestDto</h3>
<pre><code class="language-java">@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class CrewSaveRequestDto {
private String name; // 크루이름
private boolean type; //  true-온라인, false-오프라인
private boolean cost; // true-유료탑승, false-무료탑승
private String description; // 크루즈 설명
private String wisher; // 원하는 선원
private String plan; // 크루즈 설명

</code></pre>
</li>
</ul>
<p>  public Crew toEntity(){
    return Crew.builder()
        .name(name)
        .type(type)
        .cost(cost)
        .wisher(wisher)
        .plan(plan)
        .description(description)
        // 이 부분
        .isPublished(true) // 처음 crew를 만들 땐 true
        .isRecruiting(true) // 처음 crew를 만들 땐 true
        .isClosed(false) // 처음 crew를 만들 땐 false
        //
        .build();
  }
}</p>
<pre><code>- 우선 크루를 처음 만들었을땐 크루 공개 여부 true, 크루 모집 true, 크루 종료 false로 세팅했다.
### CrewResponseDto
```java
@Getter
@NoArgsConstructor
public class CrewResponseDto {

  private Long id; // 크루 조회할 때 사용
  private String leader; // 크루장
  private String name; // 크루명
  private boolean type; //  true-온라인, false-오프라인
  private boolean cost; // true-유료탑승, false-무료탑승
  private String description; // 크루즈 설명
  private String wisher; // 원하는 선원
  private String plan; // 크루즈 설명
  private boolean isRecruit; // true - 모집중, false - 모집X
  private boolean isPublished; // true - 공개O, false - 공개X
  private boolean isClosed;
  private List&lt;UserResponseDto&gt; users;

  public CrewResponseDto(Crew crew) {
    this.id = crew.getId();
    this.leader = crew.getUser().getNickname();
    this.name = crew.getName();
    this.type = crew.isType();
    this.cost = crew.isCost();
    this.description = crew.getDescription();
    this.wisher = crew.getWisher();
    this.plan = crew.getPlan();
    this.isRecruit = crew.isRecruiting();
    this.isPublished = crew.isPublished();
    this.isClosed = crew.isClosed();
    this.users = crew.getUsers().stream().map(UserResponseDto::new).collect(Collectors.toList());
  }
}</code></pre><h2 id="crewrepository">CrewRepository</h2>
<ul>
<li><p>생략 (크루 CRUD와 동일)</p>
<h2 id="crewservice">CrewService</h2>
<pre><code class="language-java">public class CrewService {
private final CrewRepository crewRepository;

...
// 크루 참가
public void addUser(Long id, User user){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  if (crew.isJoinable(user)){
    crew.addUser(user);
  }
}

public void leaveCrew(Long id, User user){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  if (crew.isMember(user)) {
    crew.removeUser(user);
  }
}

@Transactional
public void update(Long id, CrewUpdateRequestDto crewUpdateRequestDto){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  crew.update(crewUpdateRequestDto); // 이렇게 엔티티로 Dto를 넘겨도 되나?
}

// 크루 세팅 - 크루 공개 여부
public void setPublished(Long id){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  crew.setPublished();
}

// 크루 세팅 - 크루원 모집 여부
public void setRecruited(Long id){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  crew.setRecruited();
}
// 크루 세팅 - 크루 종료 여부
public void setClosed(Long id){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  crew.setClosed();
}
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>## CrewViewController
- 크루 CRUD 부분은 이 포스팅에선 생략하겠다.
```java
@Controller
@RequiredArgsConstructor
@Transactional
@Slf4j
public class CrewViewController {
  private final CrewService crewService;

  // 크루 참가 신청
  @GetMapping(&quot;/crews/{id}/join&quot;)
  public String joinCrew(HttpSession session, @PathVariable Long id) {
    User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
    crewService.addUser(id, loginUser);
    return &quot;redirect:/crews/&quot; + id;
  }

  // 크루 탈퇴
  @GetMapping(&quot;/crews/{id}/leave&quot;)
  public String leaveCrew(HttpSession session, @PathVariable Long id) {
    User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
    crewService.leaveCrew(id, loginUser);
    return &quot;redirect:/crews/&quot; + id;
  }

  // 크루 detailView
  @GetMapping(&quot;/crews/{id}/log&quot;) // 활동일지
  public String crewActivity(@PathVariable Long id, Model model, HttpSession session) {
    User user = (User) session.getAttribute(&quot;loginUser&quot;);
    CrewResponseDto crewResponseDto = crewService.findById(id);
    UserResponseDto loginUser = new UserResponseDto(user);
    model.addAttribute(&quot;loginUser&quot;, loginUser);
    model.addAttribute(&quot;crewResponseDto&quot;, crewResponseDto);
    return &quot;crew/log&quot;;
  }

  @GetMapping(&quot;/crews/{id}/meeting&quot;) // 모임
  public String crewMeeting(@PathVariable Long id, Model model, HttpSession session) {
    User user = (User) session.getAttribute(&quot;loginUser&quot;);
    CrewResponseDto crewResponseDto = crewService.findById(id);
    UserResponseDto loginUser = new UserResponseDto(user);
    model.addAttribute(&quot;loginUser&quot;, loginUser);
    model.addAttribute(&quot;crewResponseDto&quot;, crewResponseDto);
    return &quot;crew/meeting&quot;;
  }

  @GetMapping(&quot;/crews/{id}/setting&quot;) // 설정 -&gt; Crew Update
  public String crewSetting(@PathVariable Long id, Model model, HttpSession session) {
    User user = (User) session.getAttribute(&quot;loginUser&quot;);
    CrewResponseDto crewResponseDto = crewService.findById(id);
    UserResponseDto loginUser = new UserResponseDto(user);

    model.addAttribute(&quot;loginUser&quot;, loginUser);
    model.addAttribute(&quot;crewResponseDto&quot;, crewResponseDto);

    model.addAttribute(&quot;crewUpdateRequestDto&quot;, new CrewUpdateRequestDto());
    return &quot;crew/setting&quot;;
  }

  @PostMapping(&quot;/crews/{id}/setting&quot;) // 설정 -&gt; Crew Update
  public String crewSetting(@PathVariable Long id,
                            @ModelAttribute CrewUpdateRequestDto crewUpdateRequestDto) {
    crewService.update(id, crewUpdateRequestDto);
    return &quot;redirect:/crews/&quot; + id;
  }

  // 크루 공개 여부
  @PostMapping(&quot;/crews/{id}/published&quot;)
  public String publishedCrew(@PathVariable Long id) {
    crewService.setPublished(id);
    return &quot;redirect:/crews/&quot; + id + &quot;/setting&quot;;
  }
  // 크루 인원 모집 여부
  @PostMapping(&quot;/crews/{id}/recruit&quot;)
  public String recruitCrew(@PathVariable Long id) {
    crewService.setRecruited(id);
    return &quot;redirect:/crews/&quot; + id + &quot;/setting&quot;;
  }

  // 크루 종료 여부
  @PostMapping(&quot;/crews/{id}/close&quot;)
  public String closeCrew(@PathVariable Long id) {
    crewService.setClosed(id);
    return &quot;redirect:/crews/&quot; + id + &quot;/setting&quot;;
  }

}</code></pre><p>참고로 Thymeleaf는 바로 이전 게시글인 크루 CRUD와 똑같아서 크루 세팅이나 참가, 탈퇴와 관련된 부분만 가져오겠다</p>
<h2 id="thymeleaf">Thymeleaf</h2>
<h3 id="크루-소개-화면">크루 소개 화면</h3>
<pre><code class="language-html">...
&lt;h2&gt;👥크루원&lt;/h2&gt;
  &lt;p th:text=&quot;|크루장 : ${crewResponseDto.leader}|&quot;&gt;&lt;/p&gt;
  &lt;div class=&quot;member&quot; th:each=&quot;member : ${crewResponseDto.getUsers()}&quot;&gt;
    &lt;p th:text=&quot;|크루원 : ${member.nickname}|&quot;&gt;&lt;/p&gt;
  &lt;/div&gt;
...</code></pre>
<ul>
<li>크루원을 가져올 때 Crew 엔티티의 List&lt;User&gt; users = new ArrayList&lt;&gt;();를 통해 크루원들을 가져온다.</li>
</ul>
<h3 id="크루-설정">크루 설정</h3>
<pre><code class="language-html">...
&lt;div class=&quot;crew-setting&quot;&gt;
    &lt;!--크루 공개--&gt;
      &lt;form th:action=&quot;@{/crews/{id}/published(id=${crewResponseDto.id})}&quot; method=&quot;post&quot;&gt;
        &lt;button type=&quot;submit&quot;  th:text=&quot;${crewResponseDto.published} ? &#39;크루 비공개&#39; : &#39;크루원 공개&#39;&quot;&gt;크루 비공개&lt;/button&gt;
      &lt;/form&gt;
      &lt;!--크루 인원 모집 --&gt;
      &lt;form th:action=&quot;@{/crews/{id}/recruit(id=${crewResponseDto.id})}&quot; method=&quot;post&quot;&gt;
        &lt;button type=&quot;submit&quot; th:text=&quot;${crewResponseDto.recruit} ? &#39;크루원 모집 중단&#39; : &#39;크루원 모집하기&#39;&quot;&gt;&lt;/button&gt;
      &lt;/form&gt;
        &lt;!--크루 종료, 활동일지 등 이런걸 추억으로 남기고 싶을 땐 종료하고 마이페이지 관심 크루, 종료 크루, 참여 크루 보여주자....--&gt;
      &lt;form th:action=&quot;@{/crews/{id}/close(id=${crewResponseDto.id})}&quot; method=&quot;post&quot;&gt;
        &lt;button type=&quot;submit&quot; th:text=&quot;${crewResponseDto.closed} ? &#39;크루 활성화&#39; : &#39;크루 활동 종료&#39;&quot;&gt;&lt;/button&gt;
      &lt;/form&gt;
       ...
&lt;/div&gt; &lt;!--crew-setting--&gt;
...</code></pre>
<ul>
<li>이 부분은 crewResponse로부터 크루 공개 여부, 크루 모집 여부, 크루 활동 종료 여부 등의 값들을 가져와서 true, false값을 가지고 버튼의 값을 변경한다. 
<img src="https://velog.velcdn.com/images/security-won/post/23fb4ccd-3bd1-4654-ae37-4b4df57bafd7/image.png" alt="">
<img src="https://velog.velcdn.com/images/security-won/post/de6ba6da-a388-4c3c-9bf1-95e40417e8a2/image.png" alt=""></li>
</ul>
<h2 id="크루-가입">크루 가입</h2>
<ul>
<li>fragment에 크루 헤더에 관해 작성한 부분인데 다음과 같이 값이 넘어온다.<pre><code class="language-html">&lt;nav th:replace=&quot;~{fragment/fragment :: crewDetailnav(${loginUser.name}, ${crewResponseDto.leader},
${crewResponseDto.users.contains(loginUser)}, ${crewResponseDto.recruit} )}&quot;&gt;&lt;/nav&gt;</code></pre>
<pre><code class="language-html">&lt;button type=&quot;button&quot; th:if=&quot;${loginUserNicname == crewLeaderName}&quot; 
      th:onclick=&quot;|location.href=&#39;@{/crews/{crewId}/setting(crewId=${crewResponseDto.id})}&#39;|&quot;&gt;
크루 설정
&lt;/button&gt;
</code></pre>
</li>
</ul>
<p><button type="button" th:if="${isMember}"
        th:onclick="|location.href='@{/crews/{crewId}/leave(crewId=${crewResponseDto.id})}'|">
  크루 탈퇴
</button></p>
<p><button type="button" th:if="${!isMember and loginUserNicname != crewLeaderName and isRecruit}" 
        th:onclick="|location.href='@{/crews/{crewId}/join(crewId=${crewResponseDto.id})}'|">
  크루 가입
</button></p>
<pre><code>- 크루 설정은 크루장만 볼 수 있게 작성했다.
- 멤버이면 크루 탈퇴 버튼이 보이고 멤버가 아니면 크루 가입 버튼이 보인다.

#### 크루장인 경우![](https://velog.velcdn.com/images/security-won/post/27a63b89-06cf-46cb-83a0-153b7b76afe7/image.png)

#### 크루장인 아닌 경우![](https://velog.velcdn.com/images/security-won/post/d0a542fa-d816-45fe-b710-789e843c7ae7/image.png)

#### 크루원이 아닌 경우![](https://velog.velcdn.com/images/security-won/post/eb4ac660-ced8-4401-8e67-389ca13331c9/image.png)

#### 크루원인  경우![](https://velog.velcdn.com/images/security-won/post/319cbef4-563b-42ff-bf4b-35f2d5a09f1d/image.png)![](https://velog.velcdn.com/images/security-won/post/6b9c783f-d78d-43b4-8c41-44124d79bc21/image.png)
- 관심있어요 기능은 크루원인 경우에 안보이게 했다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 - 크루 CRUD]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%ED%81%AC%EB%A3%A8-CRUD</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%ED%81%AC%EB%A3%A8-CRUD</guid>
            <pubDate>Tue, 24 Oct 2023 16:11:13 GMT</pubDate>
            <description><![CDATA[<p>크루 CRUD 기능 구현에 대해 정리하고자 한다.</p>
<h2 id="crew">Crew</h2>
<pre><code class="language-java">@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
public class Crew extends BaseTimeEntity {

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

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = &quot;user_id&quot;)
  private User user; // 크루장 - 로그인한 user

  private String name; // 크루이름
  private boolean type; //  true-온라인, false-오프라인
  private boolean cost; // true-유료탑승, false-무료탑승
  private boolean isRecruiting; // true - 모집중, false - 모집X,
  private boolean isPublished; // true - 공개O, false - 공개X
  private boolean isClosed; // true - 종료
  private String thumbnail; //이미지 경로 , 처음엔 기본 이미지 -&gt; 크루 설정을 통해 배너 수정 가능

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String description; // 크루 목적

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String wisher; // 원하는 선원

  @Lob // varchar보다 클 경우 사용하는 어노테이션
  private String plan; // 크루즈 설명

  @ManyToMany
  @JoinTable(name = &quot;user_crew&quot;)
  private List&lt;User&gt; users = new ArrayList&lt;&gt;();


  public void update(CrewUpdateRequestDto crewUpdateRequestDto){
    this.name = crewUpdateRequestDto.getName();
    this.type = crewUpdateRequestDto.isType();
    this.cost = crewUpdateRequestDto.isCost();
    this.description = crewUpdateRequestDto.getDescription();
    this.wisher = crewUpdateRequestDto.getWisher();
    this.plan = crewUpdateRequestDto.getPlan();
  }

}
</code></pre>
<ul>
<li>@ManyToMany를 권장하지는 않지만 우선 존재하는 방법이기 때문에 한번 사용은 해봐야겠다는 생각이 들어 여기서는 @ManyToMany를 사용해보았다.</li>
<li>우선 크루 생성과 관련된 필드들만 나타냈다.<h2 id="dto">DTO</h2>
<h3 id="crewsaverequestdto">CrewSaveRequestDto</h3>
<pre><code class="language-java">@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class CrewSaveRequestDto {
private String name; // 크루이름
private boolean type; //  true-온라인, false-오프라인
private boolean cost; // true-유료탑승, false-무료탑승
private String description; // 크루즈 설명
private String wisher; // 원하는 선원
private String plan; // 크루즈 설명

</code></pre>
</li>
</ul>
<p>  public Crew toEntity(){
    return Crew.builder()
        .name(name)
        .type(type)
        .cost(cost)
        .wisher(wisher)
        .plan(plan)
        .description(description)
        .isPublished(true) // 처음 crew를 만들 땐 true
        .isRecruiting(true) // 처음 crew를 만들 땐 true
        .isClosed(false) // 처음 crew를 만들 땐 false
        .build();
  }
}</p>
<pre><code>- 크루 썸네일 기능은 구현중에 있다..............
### CrewResponseDto
```java
@Getter
@NoArgsConstructor
public class CrewResponseDto {

  private Long id; // 크루 조회할 때 사용
  private String leader; // 크루장
  private String name; // 크루명
  private boolean type; //  true-온라인, false-오프라인
  private boolean cost; // true-유료탑승, false-무료탑승
  private String description; // 크루즈 설명
  private String wisher; // 원하는 선원
  private String plan; // 크루즈 설명
  private boolean isRecruit; // true - 모집중, false - 모집X
  private boolean isPublished; // true - 공개O, false - 공개X
  private boolean isClosed;
  private List&lt;UserResponseDto&gt; users;

  public CrewResponseDto(Crew crew) {
    this.id = crew.getId();
    this.leader = crew.getUser().getNickname();
    this.name = crew.getName();
    this.type = crew.isType();
    this.cost = crew.isCost();
    this.description = crew.getDescription();
    this.wisher = crew.getWisher();
    this.plan = crew.getPlan();
    this.isRecruit = crew.isRecruiting();
    this.isPublished = crew.isPublished();
    this.isClosed = crew.isClosed();
    this.users = crew.getUsers().stream().map(UserResponseDto::new).collect(Collectors.toList());
  }
}</code></pre><ul>
<li>딱히 설명이 필요해 보이는 코드는 없는거같다.<h3 id="crewupdaterequestdto">CrewUpdateRequestDto</h3>
<pre><code class="language-java">@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class CrewUpdateRequestDto {
private String name; // 크루이름
private boolean type; //  true-온라인, false-오프라인
private boolean cost; // true-유료탑승, false-무료탑승
private String description; // 크루즈 설명
private String wisher; // 원하는 선원
private String plan; // 크루즈 설명
}
</code></pre>
</li>
</ul>
<pre><code>## CrewRepository
```java
public interface CrewRepository extends JpaRepository&lt;Crew, Long&gt; {

  Page&lt;Crew&gt; findAll(Pageable pageable);

}</code></pre><ul>
<li><p>크루 전체 조회시 페이징 기능을 위해 추가했다.</p>
<h2 id="crewservice">CrewService</h2>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class CrewService {
private final CrewRepository crewRepository;

// 크루 저장
public Long save(CrewSaveRequestDto crewSaveRequestDto, User user){
  Crew crew = crewSaveRequestDto.toEntity();
  crew.setUser(user);
  Crew savedCrew = crewRepository.save(crew);
  return savedCrew.getId();
}
// 크루 전체 조회
public Page&lt;CrewResponseDto&gt; findAll(Pageable pageable) {
  Page&lt;CrewResponseDto&gt; crews = crewRepository.findAll(pageable).map(CrewResponseDto::new);
  return crews;
}
// 크루 상세 조회
public CrewResponseDto findById(Long id){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  return new CrewResponseDto(crew);
}

// 크루 삭제 (크루장만 삭제 가능)
public void delete(Long id){
  crewRepository.deleteById(id);
}

// 크루 참가
public void addUser(Long id, User user){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  if (crew.isJoinable(user)){
    crew.addUser(user);
  }
}

public void leaveCrew(Long id, User user){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  if (crew.isMember(user)) {
    crew.removeUser(user);
  }
}

@Transactional
public void update(Long id, CrewUpdateRequestDto crewUpdateRequestDto){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  crew.update(crewUpdateRequestDto); 
}
}</code></pre>
</li>
<li><p>이렇게 엔티티로 Dto를 넘겨도 되는지는 아직 의문이지만 우선은 Update 정보를 담은 dto를 넘겨줌으로써 크루 수정 코드를 작성했다.</p>
<h2 id="crewviewcontroller">CrewViewController</h2>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
@Transactional
@Slf4j
public class CrewViewController {
private final CrewService crewService;

// 크루 생성 폼
@GetMapping(&quot;/crews/form&quot;)
public String crewForm(Model model) {
  model.addAttribute(&quot;crewSaveRequestDto&quot;, new CrewSaveRequestDto());
  return &quot;crew/form&quot;;
}

// 크루 생성
@PostMapping(&quot;/crews/form&quot;)
public String crewSave(@ModelAttribute CrewSaveRequestDto crewSaveRequestDto,
                       HttpSession session) {
  User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
  Long id = crewService.save(crewSaveRequestDto, loginUser);
  return &quot;redirect:/crews/&quot; + id;
}

// 크루 조회 - 상세 조회
@GetMapping(&quot;/crews/{id}&quot;) // 서비스 기본화면
public String crew(@PathVariable Long id, Model model, HttpSession session) {
  User user = (User) session.getAttribute(&quot;loginUser&quot;);
  CrewResponseDto crewResponseDto = crewService.findById(id);

  UserResponseDto loginUser = new UserResponseDto(user);

  model.addAttribute(&quot;loginUser&quot;, loginUser);
  model.addAttribute(&quot;crewResponseDto&quot;, crewResponseDto);

  return &quot;crew/intro&quot;;
}

// 크루 전체 조회 - 현재 모집 중인 크루즈
@GetMapping(/crews) // 서비스 기본화면
public String crews(Model model,
                    @PageableDefault(size = 5, sort = &quot;createdDate&quot;,
                        direction = Sort.Direction.DESC) Pageable pageable,
                    @SessionAttribute(name = &quot;loginUser&quot;, required = false) User loginUser) {
  Page&lt;CrewResponseDto&gt; crews = crewService.findAll(pageable);
  model.addAttribute(&quot;loginUser&quot;, loginUser);
  model.addAttribute(&quot;crews&quot;, crews); // 현재 모집 중인 크루즈를 나타낼 때 사용
  //model.addAttribute(&quot;top10Crew&quot;, crews); // top10크루 정보를 담고 있음 -&gt; 쿼리를 생성해서 가져와야할듯
  return &quot;main&quot;;

}
@DeleteMapping(&quot;/crews/{id}/delete&quot;)
public String deleteCrew(@PathVariable Long id){
  crewService.delete(id);
  return &quot;redirect:/&quot;;
}
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>- 아직 Top10 크루는 구현하지 않았다. 어떤 기준으로 Top10을 선정할지 고민중이다.
- 아마 Top10 크루를 가져올 때 쿼리를 직접 작성해서 가져올거같다.

## Top10 크루 가져오기 추가 예정!
- 이 부분은 아직 기능이 완성되지 않았다.
- 그래서 Top10 관련된 코드들은 무시하면 된다.

## Thymeleaf
### 크루 생성 폼
```html
&lt;!DOCTYPE html&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;Title&lt;/title&gt;
  &lt;link rel=&quot;stylesheet&quot; th:href=&quot;@{../css/crew/crewForm.css}&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
  &lt;p&gt;크루 생성 후 수정할 수 있습니다.&lt;/p&gt;
  &lt;form th:action th:object=&quot;${crewSaveRequestDto}&quot; method=&quot;post&quot;&gt;
    &lt;label for=&quot;crewName&quot;&gt;크루즈명&lt;/label&gt;
    &lt;input type=&quot;text&quot; id=&quot;crewName&quot; placeholder=&quot;크루명을 입력해주세요.&quot; th:field=&quot;*{name}&quot;&gt;

    &lt;label for=&quot;online&quot;&gt;온라인 유무&lt;/label&gt;
    &lt;input type=&quot;checkbox&quot; id=&quot;online&quot; th:field=&quot;*{type}&quot;&gt;

    &lt;label for=&quot;cost&quot;&gt;비용 유무&lt;/label&gt;
    &lt;input type=&quot;checkbox&quot; id=&quot;cost&quot; th:field=&quot;*{cost}&quot;&gt;

    &lt;label for=&quot;crewGoal&quot;&gt;크루 소개&lt;/label&gt;
    &lt;textarea type=&quot;text&quot; id=&quot;crewGoal&quot; placeholder=&quot;크루 소개를 입력해주세요.&quot; th:field=&quot;*{description}&quot;&gt;&lt;/textarea&gt;

    &lt;label for=&quot;wisher&quot;&gt;이런 선원을 원해요.&lt;/label&gt;
    &lt;textarea type=&quot;text&quot; id=&quot;wisher&quot; placeholder=&quot;원하는 선원을 입력해주세요.&quot; th:field=&quot;*{wisher}&quot;&gt;&lt;/textarea&gt;

    &lt;label for=&quot;plan&quot;&gt;항해 계획&lt;/label&gt;
    &lt;textarea type=&quot;text&quot; id=&quot;plan&quot; placeholder=&quot;항해 계획을 입력해주세요.&quot; th:field=&quot;*{plan}&quot;&gt;&lt;/textarea&gt;
    &lt;button type=&quot;submit&quot;&gt;등록&lt;/button&gt;
  &lt;/form&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre><h3 id="크루-상세-화면">크루 상세 화면</h3>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;크루 상세 화면&lt;/title&gt;
  &lt;link rel=&quot;stylesheet&quot; th:href=&quot;@{/css/crew/intro.css}&quot;&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;header th:replace=&quot;~{fragment/fragment :: crewDetailheader(${crewResponseDto.name})}&quot;&gt;&lt;/header&gt;
  &lt;nav th:replace=&quot;~{fragment/fragment :: crewDetailnav(${loginUser.name}, ${crewResponseDto.leader}
  ,${crewResponseDto.users.contains(loginUser)}, ${crewResponseDto.recruit} )}&quot;&gt;&lt;/nav&gt;

  &lt;div id=&quot;container&quot;&gt;
    &lt;h2 th:text=&quot;|안녕하세요. ${crewResponseDto.name}입니다.|&quot;&gt;&lt;/h2&gt;
    &lt;h4&gt;👍저희 크루는 이런 크루입니다.&lt;/h4&gt;
    &lt;p th:text=&quot;${crewResponseDto.description}&quot;&gt;
    &lt;h4&gt;🧐저희 크루는 이러한 사람을 원해요.&lt;/h4&gt;
    &lt;p th:text=&quot;${crewResponseDto.wisher}&quot;&gt;&lt;/p&gt;
    &lt;h4&gt;📆저희 크루의 활동 계획입니다.&lt;/h4&gt;
    &lt;p th:text=&quot;${crewResponseDto.plan}&quot;&gt;
    &lt;h2&gt;👥크루원&lt;/h2&gt;
    &lt;p th:text=&quot;|크루장 : ${crewResponseDto.leader}|&quot;&gt;&lt;/p&gt;
    &lt;div class=&quot;member&quot; th:each=&quot;member : ${crewResponseDto.getUsers()}&quot;&gt;
      &lt;p th:text=&quot;|크루원 : ${member.nickname}|&quot;&gt;&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

&lt;div th:replace=&quot;~{fragment/fragment :: footerFragment}&quot;&gt;&lt;/div&gt;
&lt;/main&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3 id="fragmenthtml">fragment.html</h3>
<ul>
<li><p>header나 버튼 등의 공통 조각들을 가지고 있다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;body&gt;
&lt;header th:fragment=&quot;boardHeader&quot;&gt;
&lt;div class=&quot;left-section&quot;&gt;
  &lt;ul&gt;&lt;a href=&quot;/boards&quot;&gt;자유게시판&lt;/a&gt;&lt;/ul&gt;
  &lt;ul&gt;&lt;a href=&quot;/crews&quot;&gt;크루 화면&lt;/a&gt;&lt;/ul&gt;
&lt;/div&gt;

&lt;div class=&quot;right-section&quot;&gt;
  &lt;button type=&quot;button&quot; th:onclick=&quot;|location.href=&#39;@{/board/form}&#39;|&quot;&gt;글 등록&lt;/button&gt; &lt;!-- 폼 태그로 감싸던가 아니면 자바스크립트--&gt;
  &lt;form th:action=&quot;@{/logout}&quot; th:method=&quot;post&quot;&gt;
    &lt;button type=&quot;submit&quot;&gt;로그아웃&lt;/button&gt;
  &lt;/form&gt;
  &lt;button type=&quot;button&quot; th:onclick=&quot;|location.href=&#39;@{/my-page}&#39;|&quot;&gt;마이페이지&lt;/button&gt;
&lt;/div&gt;
&lt;/header&gt;
</code></pre>
</li>
</ul>
<header th:fragment="crewDetailheader(name)">
  <div class="left-section">
    <ul><a href="/boards">자유게시판</a></ul>
    <ul><a href="/crews">크루 화면</a></ul>
  </div>
  <div class="middle-section">
    <h1 th:text="${name}"></h1>
  </div>
  <div class="right-section">
    <form th:action="@{/logout}" th:method="post">
      <button type="submit">로그아웃</button>
    </form>
    <button type="button" th:onclick="|location.href='@{/my-page}'|">마이페이지</button>
  </div>
</header>

<nav class="info" th:fragment="crewDetailnav(loginUserNicname, crewLeaderName,isMember, isRecruit)">
  <div class="intro-btn">
    <button type="button" th:onclick="|location.href='@{/crews/{id}(id=${crewResponseDto.id})}'|">
      소개
    </button>
    <button type="button" th:onclick="|location.href='@{/crews/{id}/log(id=${crewResponseDto.id})}'|">
      활동 일지
    </button>
    <button type="button" th:onclick="|location.href='@{/crews/{id}/meeting(id=${crewResponseDto.id})}'|">
      모임
    </button>
    <button type="button" th:if="${loginUserNicname == crewLeaderName}"
            th:onclick="|location.href='@{/crews/{crewId}/setting(crewId=${crewResponseDto.id})}'|">크루 설정
    </button>
  </div>
  <div class="crew-btn">  <!--관심있어요 -> 맴버가 아니고, 공개된 크루이고, 회원 모집중일때만 가능하게 하기.-->
    <button type="button" th:if="${!isMember}"
            th:onclick="|location.href='@{/crews/{crewId}/like(crewId=${crewResponseDto.id})}'|">관심있어요.
    </button>
    <button type="button" th:if="${isMember}"
            th:onclick="|location.href='@{/crews/{crewId}/leave(crewId=${crewResponseDto.id})}'|">크루 탈퇴
    </button>
    <button type="button" th:if="${!isMember and loginUserNicname != crewLeaderName and isRecruit}"
            th:onclick="|location.href='@{/crews/{crewId}/join(crewId=${crewResponseDto.id})}'|">크루 가입
    </button>
  </div>
</nav>

<footer th:fragment="footerFragment">
  <p> COPYRIGHT@ JIWON27</p>
</footer>
</body>
</html>
```
### update form
```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>크루 상세 화면</title>
  <link rel="stylesheet" th:href="@{/css/crew/setting.css}">
</head>
<body>
<header th:replace="~{fragment/fragment :: crewDetailheader(${crewResponseDto.name})}"></header>
<nav th:replace="~{fragment/fragment :: crewDetailnav(${loginUser.name}, ${crewResponseDto.leader},
${crewResponseDto.users.contains(loginUser)}, ${crewResponseDto.recruit} )}"></nav>

<div id="container">
  <div class="setting">
    <div class="crew-setting">
      <!--크루 공개-->
      <form th:action="@{/crews/{id}/published(id=${crewResponseDto.id})}" method="post">
        <button type="submit"  th:text="${crewResponseDto.published} ? '크루 비공개' : '크루원 공개'">크루 비공개</button>
      </form>
      <!--크루 인원 모집 -->
      <form th:action="@{/crews/{id}/recruit(id=${crewResponseDto.id})}" method="post">
        <button type="submit" th:text="${crewResponseDto.recruit} ? '크루원 모집 중단' : '크루원 모집하기'"></button>
      </form>
        <!--크루 종료, 활동일지 등 이런걸 추억으로 남기고 싶을 땐 종료하고 마이페이지 관심 크루, 종료 크루, 참여 크루 보여주자....-->
      <form th:action="@{/crews/{id}/close(id=${crewResponseDto.id})}" method="post">
        <button type="submit" th:text="${crewResponseDto.closed} ? '크루 활성화' : '크루 활동 종료'"></button>
      </form>
        <!--크루 삭제-->
      <form th:action="@{/crews/{id}/delete(id=${crewResponseDto.id})}" method="delete">
        <button type="submit">크루 삭제</button>
      </form>
    </div> <!--crew-setting-->

<pre><code>&lt;form th:action th:object=&quot;${crewUpdateRequestDto}&quot; method=&quot;post&quot;&gt;
  &lt;div class=&quot;text&quot;&gt;
    &lt;label for=&quot;crewName&quot;&gt;크루즈명&lt;/label&gt;
    &lt;input type=&quot;text&quot; id=&quot;crewName&quot; placeholder=&quot;크루명을 수정합니다.&quot; th:field=&quot;*{name}&quot;&gt;
  &lt;/div&gt;
  &lt;div class=&quot;checkbox&quot;&gt;
    &lt;label for=&quot;online&quot;&gt;온라인 유무(체크O - 온라인, 체크X - 오프라인)&lt;/label&gt;
    &lt;input type=&quot;checkbox&quot; id=&quot;online&quot; th:field=&quot;*{type}&quot;&gt;
  &lt;/div&gt;

  &lt;div class=&quot;checkbox&quot;&gt;
    &lt;label for=&quot;cost&quot;&gt;비용 유무(체크O - 유료, 체크X - 무료)&lt;/label&gt;
    &lt;input type=&quot;checkbox&quot; id=&quot;cost&quot; th:field=&quot;*{cost}&quot;&gt;
  &lt;/div&gt;

  &lt;div class=&quot;text&quot;&gt;
    &lt;label for=&quot;crewGoal&quot;&gt;크루 소개&lt;/label&gt;
    &lt;textarea type=&quot;text&quot; id=&quot;crewGoal&quot; placeholder=&quot;크루 소개를 수정합니다.&quot; th:field=&quot;*{description}&quot;&gt;&lt;/textarea&gt;
  &lt;/div&gt;

  &lt;div class=&quot;text&quot;&gt;
    &lt;label for=&quot;wisher&quot;&gt;이런 선원을 원해요.&lt;/label&gt;
    &lt;textarea type=&quot;text&quot; id=&quot;wisher&quot; placeholder=&quot;원하는 선원을 수정합니다.&quot; th:field=&quot;*{wisher}&quot;&gt;&lt;/textarea&gt;
  &lt;/div&gt;

  &lt;div class=&quot;text&quot;&gt;
    &lt;label for=&quot;plan&quot;&gt;크루 계획&lt;/label&gt;
    &lt;textarea type=&quot;text&quot; id=&quot;plan&quot; placeholder=&quot;크루 계획을 수정합니다.&quot; th:field=&quot;*{plan}&quot;&gt;&lt;/textarea&gt;
  &lt;/div&gt;
&lt;button type=&quot;submit&quot;&gt;수정&lt;/button&gt;
&lt;/form&gt;</code></pre>  </div><!--setting-->
</div> <!--container-->

<div th:replace="~{fragment/fragment :: footerFragment}"></div>
</main>
</body>
</html>
```
- 여기엔 다음 포스팅에서 정리할 크루 공개/비공개, 크루 모집 등의 설정하는 부분ㄷ 있지만 우선 크루 삭제하는 부분인 크루 삭제 버튼과 수정 폼을 집중적으로 보면된다.
### main 화면
```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>main</title>
  <link rel="stylesheet" th:href="@{css/main.css}">
</head>
<body>
<!-- 로그인 성공 후 메인 화면-->
<header>
  <div class="left-section">
    <ul><a href="/boards">자유게시판</a></ul>
    <ul><a href="#this">크루 화면</a></ul>
  </div>

  <div class="right-section">
    <button type="submit" th:onclick="|location.href='@{/crew/form}'|" th:if="${loginUser != null}">크루 생성</button>
    <button type="submit" th:onclick="|location.href='@{/logout}'|" th:if="${loginUser != null}">로그아웃</button>
    <button type="submit" th:onclick="|location.href='@{/my-page}'|" th:if="${loginUser != null}">마이페이지</button>
    <button type="submit" th:onclick="|location.href='@{/login}'|" th:if="${loginUser == null}">로그인</button>
    <button type="submit" th:onclick="|location.href='@{/sign-up}'|" th:if="${loginUser == null}">회원가입</button>
  </div>
</header>
<h3>Top 10 크루</h3>
<div class="top10_crew">
  <div class="crew"  th:each="crew : ${crews}" th:if="${crew.isPublished && !crew.closed}">
    <img src="" alt="" width="150px" height="100px"><br>
    <a th:href="@{/crews/{crewId}(crewId=${crew.id})}" th:text="${crew.name}"></a><br>
    <span th:text="${crew.leader}"></span> <br>
    <span th:text="${crew.type} ? '온라인 활동' : '오프라인 활동'"></span><br> <!--crew.type=true면 온라인-->
    <span th:text="${crew.cost} ? '유료' : '무료'"></span><br> <!--crew.cost=true면 유료-->
  </div>

</div>
<h3>현재 모집 중인 크루즈</h3>
  <div class="crew-container">
    <div class="crew"  th:each="crew : ${crews}" th:if="${crew.isPublished && !crew.closed}">
      <img src="" alt="" width="150px" height="100px"><br>
      <a th:href="@{/crews/{crewId}(crewId=${crew.id})}" th:text="${crew.name}"></a><br>
      <span th:text="${crew.leader}"></span> <br>
      <span th:text="${crew.type} ? '온라인 활동' : '오프라인 활동'"></span><br> <!--crew.type=true면 온라인-->
      <span th:text="${crew.cost} ? '유료' : '무료'"></span><br> <!--crew.cost=true면 유료-->
    </div>
  </div>

  <div class="page-btn">
    <a th:if="${!crews.isFirst()}" th:href="@{/crews(page=${crews.getNumber() - 1})}">이전 페이지</a>
    <a th:if="${!crews.isLast()}" th:href="@{/crews(page=${crews.getNumber() + 1})}"}>다음 페이지</a>
  </div>
</body>
</html>
```
- 크루 설정에 관한 부분도 있지만 우선 우선 크루 CRUD와 관련해서 집중적으로 보면 된다.
- img는 크루 썸네일 관련한 부분이라 우선은 img로 해두었다. 빨리 해야지,....
- CRUD에 관련해서는 딱히 설명한 부분은 없는거같다.

<p>아직 해당 웹 서비스 컨셉을 고민중이어서 단어 컨셉(?)이 일치하지 않을 수 있습니다...
디자인은 마지막에...</p>
<h2 id="화면">화면</h2>
<h3 id="main-화면">main 화면</h3>
<p><img src="https://velog.velcdn.com/images/security-won/post/8a5a62c4-5e19-4317-b331-f3659df38ce4/image.png" alt=""></p>
<h3 id="크루-form-화면">크루 form 화면</h3>
<p><img src="https://velog.velcdn.com/images/security-won/post/bb26ec67-5101-49d8-9628-69544769ab1c/image.png" alt=""></p>
<h3 id="크루-상세-화면-1">크루 상세 화면</h3>
<ul>
<li>크루 생성 후 상세 화면으로 redirect</li>
</ul>
<p><img src="https://velog.velcdn.com/images/security-won/post/652c3977-d46f-44de-b07c-9273917e6348/image.png" alt=""></p>
<h3 id="크루-수정-및-삭제">크루 수정 및 삭제</h3>
<ul>
<li><p>header부분은 sticky로 설정해서 그렇구 추후에 수정할 생각입니다.. 디자인 ... ㅠㅠ
<img src="https://velog.velcdn.com/images/security-won/post/ff35cb61-7afb-4768-a6a7-1ff104d1b07a/image.png" alt=""></p>
</li>
<li><p>수정 폼에 수정할 내용으로 입력하면 다음과 같이 수정된다
<img src="https://velog.velcdn.com/images/security-won/post/83642153-4d76-413b-8023-35aeca6c4b33/image.png" alt=""></p>
</li>
<li><p>크루 삭제 버튼을 누르면 크루가 삭제된다. 참고로 크루장만 크루 삭제가 가능하다.</p>
</li>
</ul>
<p>포스팅 중에 경로를 crew -&gt; crews로 변경했고 포스팅 내용에도 수정을 했지만 수정이 덜 된 부분이 있을 수 있어 혹시 404에러가 발생하신다면 crews로 변경하시면 됩니다.</p>
<hr>
<p>아직 부족한 코드이고 공부하면서 작성하고있습니다. 조언 너무 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 - 크루 가입 및 탈퇴 기능 중 객체의 동등성 비교]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%ED%81%AC%EB%A3%A8-%EA%B0%80%EC%9E%85-%EB%B0%8F-%ED%83%88%ED%87%B4-%EA%B8%B0%EB%8A%A5-%EC%A4%91-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EB%8F%99%EB%93%B1%EC%84%B1-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%ED%81%AC%EB%A3%A8-%EA%B0%80%EC%9E%85-%EB%B0%8F-%ED%83%88%ED%87%B4-%EA%B8%B0%EB%8A%A5-%EC%A4%91-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EB%8F%99%EB%93%B1%EC%84%B1-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Mon, 23 Oct 2023 08:36:11 GMT</pubDate>
            <description><![CDATA[<p>크루 가입 탈퇴 기능을 구현하다가 객체의 비교에 있어서 정리하고자한다.</p>
<h2 id="crew">Crew</h2>
<pre><code class="language-java">// 크루에 가입이 가능한지
  public boolean isJoinable(User user) { // (1)
    return this.isPublished() &amp;&amp; this.isRecruiting() &amp;&amp; !this.users.contains(user);
  }
  // 크루 멤버인지
  public boolean isMember(User user) { // (2)
    if (this.user.equals(user)){ // 크루장인 경우
      return true;
    }
    return this.users.contains(user); // 크루원인 경우
  }</code></pre>
<ul>
<li><p>크루 엔티티에 크루 가입 가능 여부와 크루 멤버인지에 대해서 작성했다.</p>
<h2 id="crewviewcontroller">CrewViewController</h2>
<pre><code class="language-java">// 크루 참가 신청
@GetMapping(&quot;/crew/{id}/join&quot;)
public String joinCrew(HttpSession session, @PathVariable Long id){
  User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
  crewService.addUser(id, loginUser);
  return &quot;redirect:/crews/&quot;+id;
}
// 크루 탈퇴
@GetMapping(&quot;/crew/{id}/leave&quot;)
public String leaveCrew(HttpSession session, @PathVariable Long id){
  User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
  crewService.leaveCrew(id, loginUser);
  return &quot;redirect:/crews/&quot;+id;
}</code></pre>
</li>
<li><p>컨트롤러는 위와 같이 작성했다.</p>
<h2 id="crewservice">CrewService</h2>
<pre><code class="language-java">// 크루 참가
public void addUser(Long id, User user){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  log.info(&quot;addUser before : isMember() = {}&quot;,crew.isMember(user));
  if (crew.isJoinable(user)){
    crew.addUser(user);
  }
  log.info(&quot;addUser after : isMember() = {}&quot;,crew.isMember(user));

}
public void leaveCrew(Long id, User user){
  Crew crew = crewRepository.findById(id).orElseThrow(IllegalArgumentException::new);
  log.info(&quot;leaveCrew before : isMember() = {}&quot;,crew.isMember(user)); // 왜 여기서 false지?
  if (crew.isMember(user)) {
    crew.removeUser(user);
  }
  log.info(&quot;leaveCrew after : isMember() = {}&quot;,crew.isMember(user));
}</code></pre>
</li>
<li><p>crewService는 위와 같이 작성했는데 자꾸 가입은 되도 탈퇴가 되지 않았다.</p>
</li>
<li><p>문제는 User 객체의 동등성 문제였다.</p>
</li>
<li><p>User 클래스에서 equals 및 hashCode 메서드를 적절하게 재정의하지 않아서 발생한 문제였고 User 엔티티에 다음과 같이  equals 및 hashCode 메서드 재정의해주었다.</p>
<h2 id="user">User</h2>
<pre><code class="language-java">@Override
public boolean equals(Object o) {
  if (this == o) return true;
  if (o == null || getClass() != o.getClass()) return false;
  User user = (User) o;
  return Objects.equals(getId(), user.getId()) &amp;&amp; Objects.equals(getName(), user.getName()) &amp;&amp; Objects.equals(getNickname(), user.getNickname()) &amp;&amp; Objects.equals(getPassword(), user.getPassword()) &amp;&amp; Objects.equals(getEmail(), user.getEmail());
}

@Override
public int hashCode() {
  return Objects.hash(getId(), getName(), getNickname(), getPassword(), getEmail());
}</code></pre>
</li>
<li><p>equals() 메서드를 재정의하지 않으면 Java는 기본적으로 두 객체의 동일성(즉, 두 객체가 정확히 같은 메모리 위치를 참조하는 경우)을 확인한다.</p>
</li>
<li><p>그러나 두 객체가 동일한 값을 나타내고 서로 다른 메모리 위치를 참조할 때, equals()를 사용하여 이러한 객체를 비교할 수 없다.</p>
</li>
<li><p>그래서 equals() 메서드를 재정의하여 객체의 논리적 동등성을 확인할 수 있게 만들어야한다.</p>
</li>
<li><p>뿐만 아니라, 동일한 객체를 검색하려면 동일한 해시 코드를 가져야하기때문에 hashCode() 메서드도 equals() 메서드와 일관성을 유지를 위해 재정의를 해주어야한다.</p>
</li>
<li><p>직접 재정의 하는건 실수할 가능성이 있으니 IDE에서 제공해주는 기능을 사용하자.</p>
</li>
<li><p>인텔리제이에서는 다음과 같이 equals() and hashcode()를 재정의해주는 기능이 있다.
<img src="https://velog.velcdn.com/images/security-won/post/ea37f6af-5068-4c3f-b5b6-f0b885b839d3/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 첨부파일 기능 추가 ]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B2%A8%EB%B6%80%ED%8C%8C%EC%9D%BC-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B2%A8%EB%B6%80%ED%8C%8C%EC%9D%BC-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80</guid>
            <pubDate>Thu, 19 Oct 2023 03:43:25 GMT</pubDate>
            <description><![CDATA[<p>게시글을 작성할 때 이미지 파일도 업로드 가능하게 기능을 추가하였다.
참고로 파일 업로드를 구현할 때 폼에 enctype=&quot;multipart/form-data&quot;를 추가해줘야한다.</p>
<p><del>해시태그는 구현중이다........</del></p>
<h2 id="applicationyaml">application.yaml</h2>
<ul>
<li>이름엔 제 이름이 들어있어서 우선 저렇게 작성했습니다.<pre><code>file:
dir: /Users/이름/Desktop/file-upload/</code></pre></li>
</ul>
<h3 id="board">Board</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends BaseTimeEntity {

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

  private String title;

  @Lob
  private String content;

  @ManyToOne(fetch = FetchType.LAZY) &quot;user&quot; 추가해주면 됨.
  @JoinColumn(name = &quot;user_id&quot;)
  private User user;

  @OneToMany(mappedBy = &quot;board&quot;, fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) // 글이 삭제되면 댓글 모두 삭제
  @OrderBy(&quot;createdDate&quot;)
  private List&lt;Reply&gt; replies = new ArrayList&lt;&gt;();

  @OneToMany(mappedBy = &quot;board&quot;,fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  private List&lt;ImgFile&gt; imgFiles = new ArrayList&lt;&gt;(); 

  @OneToMany(mappedBy = &quot;board&quot;, fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
  private List&lt;Tag&gt; tags = new ArrayList&lt;&gt;();

  public void setUser(User user){
    this.user = user;
  }

  public void setImgFile(List&lt;ImgFile&gt; imgFiles){
    this.imgFiles = imgFiles;
    for (ImgFile file : imgFiles) {
      file.setBoard(this);
    }
  }

  public void addTag(Tag tag){
    tags.add(tag);
    tag.setBoard(this);
  }

  public void update(String title, String content) {
    this.title = title;
    this.content = content;
  }

  @Builder
  public Board(String title, String content) {
    this.title = title;
    this.content = content;
  }
}
</code></pre>
<ul>
<li><p>Board엔티티인데 Board랑 imgFiles는 1:N 관계이다. </p>
</li>
<li><p>Board랑 imgFiles는 양방향 관계라서 연관관계 편의 메서드인 setImgFile(...)을 작성해줬는데 뭔가 아쉽다.....좀 더 연관관계 편의 메서드에 대해서 찾아봐야겠다.</p>
<h2 id="imgfile">ImgFile</h2>
<pre><code class="language-java">@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED) // 이거 추가해주니까 되네... 이유가..
public class ImgFile extends BaseTimeEntity {

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

private String originFilename; // 업로드된 파일 이름
private String storeFilename; // 저장한 파일명 ex) 132-5sdf-23451-1as.png -&gt; UUID로 식별자 생성 + 확장자

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;board_id&quot;)
private Board board;

@Builder
public ImgFile(String originFilename, String storeFilename) {
  this.originFilename = originFilename;
  this.storeFilename = storeFilename;
}

public void setBoard(Board board) {
  this.board = board;
}
}</code></pre>
<h2 id="filestore">FileStore</h2>
</li>
<li><p>파일 처리 관련 코드만을 모아두었다.</p>
</li>
<li><p>파일을 업로드한 파일명은 중복될 수 있기때문에 UUID를 활용하여 DB엔 &quot;UUID + 확장자&quot;로 파일명을 변경하여 DB에 파일명만 저장한다.</p>
</li>
<li><p>파일 자체를 DB에 저장하지않고 보통 스토리지에 저장한다. AWS로 예를 들자면 S3에 저장한다.</p>
</li>
<li><p>그래서 DB엔 파일 자체를 저장하지 않는다.</p>
<pre><code class="language-java">@Component
public class FileStore {

@Value(&quot;${file.dir}&quot;) // application.yaml or application.properties에 지정한 파일 경로
private String fileDir;

// 파일 저장 경로 Full
public String getFullPath(String filename){
  return fileDir + filename;
}

public ImgFile storeFile(MultipartFile multipartFile) throws IOException {
  if (multipartFile.isEmpty()){
    return null;
  }
  String originalFileName = multipartFile.getOriginalFilename();
  String ext = extractedExt(originalFileName);
  String storeFileName = createStoreFileName(ext);
  multipartFile.transferTo(new File(getFullPath(storeFileName)));

  return ImgFile.builder()
      .originFilename(originalFileName)
      .storeFilename(storeFileName)
      .build();
}
// 여러 개 업로드 하는 경우
public List&lt;ImgFile&gt; storeFiles(List&lt;MultipartFile&gt; multipartFiles) throws IOException{
  List&lt;ImgFile&gt; files = new ArrayList&lt;&gt;();
  for (MultipartFile multipartFile : multipartFiles) {
    if (multipartFile.isEmpty()) {
      return null;
    }
    files.add(storeFile(multipartFile));
  }
  return files;
}

// UUID를 활용해 저장용 파일명
private String createStoreFileName(String ext) {
  String uuid = UUID.randomUUID().toString();
  String storedFileName = uuid + &quot;.&quot; + ext;
  return storedFileName;
}

// 파일 확장자 추출
private String extractedExt(String originalFileName) {
  int pos = originalFileName.lastIndexOf(&quot;.&quot;);
  String ext = originalFileName.substring(pos + 1);
  return ext;
}
}</code></pre>
<h2 id="fileservice">FileService</h2>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class FileService {
private final FileStore fileStore;
private final FileRepository fileRepository; 

public List&lt;ImgFile&gt; transferImgFile(List&lt;MultipartFile&gt; imgFile) throws IOException {
  List&lt;ImgFile&gt; imgFiles = fileStore.storeFiles(imgFile);
  return imgFiles;
}

public void deleteBeforeFile(Board board){
  if (!board.getImgFiles().isEmpty()) {
    // 이전 첨부파일 삭제
    if (!board.getImgFiles().isEmpty()) {
      for (ImgFile imgFile : board.getImgFiles()) {
        // 파일 시스템에서 실제 이미지 파일 삭제
        File imgFileOnDisk = new File(fileStore.getFullPath(imgFile.getStoreFilename()));
        if (imgFileOnDisk.exists()) {
          imgFileOnDisk.delete();
        }
        // 데이터베이스에서 이미지 파일 레코드 삭제
        this.delete(imgFile);
      }
    }
  }
}
public void delete(ImgFile imgFile){
  fileRepository.delete(imgFile);
}
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>## FileRepository
```java
public interface FileRepository extends JpaRepository&lt;ImgFile, Long&gt; {

}</code></pre><h2 id="boardsaverequestdto">BoardSaveRequestDto</h2>
<pre><code class="language-java">@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class BoardSaveRequestDto {
  private String title;
  private String content;
  private List&lt;MultipartFile&gt; imgFiles;

  public Board toEntity() {
    return Board.builder()
        .title(title)
        .content(content)
        .imgFiles(new ArrayList&lt;&gt;())
        .build();
  }
}</code></pre>
<h2 id="boardcontroller">BoardController</h2>
<ul>
<li><p>게시글 등록 관련한 부분만 가져왔다.</p>
<pre><code class="language-java">...
// 글 등록
@PostMapping(&quot;/board/form&quot;)
public String boardSave(@ModelAttribute BoardSaveRequestDto boardSaveRequestDto,
                        @RequestBody TagSaveRequestDto tagSaveRequestDto, // 해시태그
                        HttpSession session) throws IOException {
  User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
  boardService.save(boardSaveRequestDto, loginUser);
  return  &quot;redirect:/boards&quot;;
}
...</code></pre>
<h2 id="boardservice">BoardService</h2>
</li>
<li><p>파일 처리 관련 부분만 가져왔다.</p>
<pre><code class="language-java">// 게시판 등록
private final BoardRepository boardRepository;
private final FileService FileService;

...
@Transactional
public Long save(BoardSaveRequestDto request, User user) throws IOException {
  List&lt;ImgFile&gt; imgFiles = FileService.transferImgFile(request.getImgFiles());// List&lt;multipartfile&gt; -&gt; List&lt;ImgFile&gt;
  Board board = request.toEntity(); // title, content
  board.setUser(user);
  if (imgFiles == null){
    board.setImgFile(new ArrayList&lt;&gt;()); // 첨부파일이 없더라도 게시글이 저장이 되도록하기 위해서 추가함. 
  } else {
    board.setImgFile(imgFiles); //
  }
  Board savedBoard = boardRepository.save(board);
  return savedBoard.getId();
}</code></pre>
</li>
</ul>
<p>다음은 게시글을 수정할 때의 로직이다.</p>
<h2 id="boardservice-1">BoardService</h2>
<ul>
<li><p>파일 처리 관련 부분만 가져왔다.</p>
<pre><code class="language-java">// 게시판 등록
// 게시글 수정
@Transactional
public void update(Long id, BoardUpdateRequestDto request) throws IOException {
  Board board = boardRepository.findById(id)
      .orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 포스트입니다.&quot;));

  List&lt;ImgFile&gt; imgFiles = FileService.transferImgFile(request.getImgFiles()); // 새로 업로드한 파일
  FileService.deleteBeforeFile(board); // 이전에 업로드 한 파일 삭제

  // 수정한 첨부파일로 연관관계 매핑
  board.setImgFile(imgFiles);
  board.update(request.getTitle(), request.getContent());</code></pre>
</li>
</ul>
<h2 id="실행-화면">실행 화면</h2>
<ul>
<li>화면에 출력하는 html 코드는 생략하겠다.
<img src="https://velog.velcdn.com/images/security-won/post/f48fbcb5-e862-453c-82f3-49147e2c7b90/image.gif" alt=""></li>
</ul>
<h3 id="스토리지">스토리지</h3>
<p><img src="https://velog.velcdn.com/images/security-won/post/1197c5c3-4a14-49c2-a408-b646bb36d99c/image.png" alt=""></p>
<h3 id="데이터베이스">데이터베이스</h3>
<p><img src="https://velog.velcdn.com/images/security-won/post/26ca3c32-60c4-4715-96dd-9a4aa92127ad/image.png" alt=""></p>
<p>뭔가 부족함이 많은 코드인거같다. 계속 공부하면서 리팩토링 해야겠다.
피드백을 주시면 감사하겠습니다.........</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티  Trouble Shooting - MultipleBagFetchException]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-Trouble-Shooting-MultipleBagFetchException</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-Trouble-Shooting-MultipleBagFetchException</guid>
            <pubDate>Tue, 17 Oct 2023 03:09:35 GMT</pubDate>
            <description><![CDATA[<p>해시 태그 구현 중에 다음과 같이 에러가 발생했다.</p>
<pre><code>Invocation of init method failed; nested exception isjavax.persistence.PersistenceException:[PersistenceUnit: default]
Unable to build Hibernate SessionFactory;nested exception is org.hibernate.loader.MultipleBagFetchException:
cannot simultaneously fetch multiple bags: [com.jiwon.blog.domain.board.Board.tags, com.jiwon.blog.domain.board.Board.replies]</code></pre><h2 id="board">Board</h2>
<pre><code class="language-java">@Entity @Getter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class Board extends BaseTimeEntity {

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

  private String title;
  @Lob
  private String content;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = &quot;user_id&quot;)
  private User user;

  @OneToMany(mappedBy = &quot;board&quot;, fetch = FetchType.EAGER, cascade = CascadeType.REMOVE) // 글이 삭제되면 댓글 모두 삭제
  @OrderBy(&quot;createdDate&quot;)
  private List&lt;Reply&gt; replies = new ArrayList&lt;&gt;();

  @OneToMany(mappedBy = &quot;board&quot;, fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
  private List&lt;Tag&gt; tags = new ArrayList&lt;&gt;();

  public void setUser(User user){ 
    this.user = user;
  }

  public void update(BoardUpdateRequestDto request) {
    this.title = request.getTitle();
    this.content = request.getContent();
  }
}</code></pre>
<h2 id="hibernate-multiplebagfetchexception">Hibernate MultipleBagFetchException</h2>
<p>찾아보니 OneToMany, ManyToMany인 Bag 두 개 이상을 EAGER로 fetch할 때 발생하는 에러라고 한다. 
위 코드에선 OneToMany인 replies와 tags를 EAGER로 fetch해서 발생한 것이었다.</p>
<h3 id="bag">Bag</h3>
<p>Bag 2개 이상.. Bag란 무엇인가
Bag(Multiset)은 Set과 같이 순서가 없고, List와 같이 중복을 허용하는 자료구조라고 한다.
하지만 자바 컬렉션 프레임워크에서는 Bag가 없기 때문에 하이버네이트에서는 List를 Bag으로써 사용하고 있는 것이다고 한다.</p>
<p>해시 태그는 보통 중복이 발생하지 않으니 List로 된 형식을 Set으로 바꿔서 해결할 수 있는데 프로젝트에선 tagify 라이브러리를 사용해서 해시 태그를 구현했다.
사용해보니 화면에서 값이 넘어올 때 중복 해시 태그가 제거된 상태로 넘어와서 해시 태그도 Set타입이 아닌 List타입으로 사용했었다.</p>
<p> 그렇다면 왜 OneToMany, ManyToMany인 Bag 두 개 이상을 EAGER로 fetch할 때 발생하는것인가?</p>
<h2 id="이유">이유</h2>
<p>해시 태그를 set자료형으로 바꾸고 실행해보면 MultipleBagFetchException이 발생하지 않는다.</p>
<ul>
<li>하이버네이트에서는 list를 Bag타입으로 취급한다고 했다.</li>
<li>Bag는 순서도 없고 중복을 허용하는 자료구조이다. </li>
<li>OneToMany, ManyToMany인 Bag(list) 두 개 이상을 가져오면 발생한다고 했다.</li>
<li>순서도 보장해주지 않고 중복도 허용하는 자료구조에서 어떤 기준으로 join을 할 지에 대한 기준도 명확하지 않기 때문에 발생하는 에러인거 같다.</li>
</ul>
<h2 id="해결">해결</h2>
<p>그래서 해결법에 대한 것을 찾아보다가 다음과 같이 정리해주신 분의 블로그를 읽으면서 해결했다. <a href="https://jojoldu.tistory.com/457">MultipleBagFetchException 발생시 해결 방법</a></p>
<ol>
<li><p>fetch 타입을 LAZY로 변경</p>
</li>
<li><p>@Entitygraph 사용</p>
</li>
<li><p>bachsize 지정</p>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends BaseTimeEntity {

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

private String title;
@Lob
private String content;

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = &quot;user_id&quot;)
private User user;

@OneToMany(mappedBy = &quot;board&quot;, fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) // 글이 삭제되면 댓글 모두 삭제
@OrderBy(&quot;createdDate&quot;)
private List&lt;Reply&gt; replies = new ArrayList&lt;&gt;();

@OneToMany(mappedBy = &quot;board&quot;, fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private Set&lt;Tag&gt; tags = new HashSet&lt;&gt;();

// 연관관계 편의 메서드 ...(?) 맞나? 잘 모르겠다..
public void addTags(Tag tag){
 this.tags.add(tag);
}

public void setUser(User user){ // 연관관계 편의 메서드
 this.user = user;
}

public void update(BoardUpdateRequestDto request) {
 this.title = request.getTitle();
 this.content = request.getContent();
}
}</code></pre>
<pre><code class="language-java">@Repository
public interface BoardRepository extends JpaRepository&lt;Board, Long&gt; {

@Override
@EntityGraph(attributePaths = {&quot;replies&quot;,&quot;tags&quot;}) //{}안에 있는 것들을 fetch join해서 가져오겠다.
List&lt;Board&gt; findAll();

Page&lt;Board&gt; findAll(Pageable pageable);
}
</code></pre>
</li>
</ol>
<pre><code>```yaml
...
  jpa:
    hibernate:
      ddl-auto: create # ???? ??? drop? ???
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        default_batch_fetch_size: 1000 # multibagfetchException 해결
...</code></pre><hr>
<p>reference
<a href="https://perfectacle.github.io/2019/05/01/hibernate-multiple-bag-fetch-exception/">(Troubleshooting) Hibernate MultipleBagFetchException 정복하기</a></p>
<p><a href="https://jojoldu.tistory.com/457">MultipleBagFetchException 발생시 해결 방법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 (4) 댓글 등록, 삭제, 수정]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-4-%EB%8C%93%EA%B8%80-%EB%93%B1%EB%A1%9D-%EB%B0%8F-%EC%82%AD%EC%A0%9C-%EC%88%98%EC%A0%95%EB%AF%B8%EC%99%84</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-4-%EB%8C%93%EA%B8%80-%EB%93%B1%EB%A1%9D-%EB%B0%8F-%EC%82%AD%EC%A0%9C-%EC%88%98%EC%A0%95%EB%AF%B8%EC%99%84</guid>
            <pubDate>Sun, 15 Oct 2023 13:54:43 GMT</pubDate>
            <description><![CDATA[<h2 id="user">User</h2>
<pre><code class="language-java">@Entity @Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = &quot;users&quot;)
public class User extends BaseTimeEntity {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = &quot;user_id&quot;)
  private Long id;
  private String name; // 본명
  private String nickname; // 별명
  private String password;
  private String email;

  @Builder
  public User(String name, String nickname, String password, String email) {
    this.name = name;
    this.nickname = nickname;
    this.password = password;
    this.email = email;
  }

  public void update(UserUpdateRequestDto updateDto) {
    this.nickname = updateDto.getNickname();
    this.password = updateDto.getPassword();
  }
}</code></pre>
<h2 id="reply">Reply</h2>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Reply extends BaseTimeEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Lob
  private String comment;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = &quot;user_id&quot;)
  private User user;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = &quot;board_id&quot;)
  private Board board;

  public void update(ReplyUpdateRequestDto updateRequestDto) {
    this.comment = updateRequestDto.getComment();
  }

  public void setUser(User user){
    this.user = user;
  }
  // 연관관계 편의 메서드
  public void setBoard(Board board){
    if(this.board != null) {
      this.board.getReplies().remove(this);
    }
    this.board = board;
    this.board.getReplies().add(this);
  }
}
</code></pre>
<h2 id="board">Board</h2>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends BaseTimeEntity {

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

  private String title;
  @Lob
  private String content;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = &quot;user_id&quot;)
  private User user;

  @OneToMany(mappedBy = &quot;board&quot;, fetch = FetchType.EAGER, cascade = CascadeType.REMOVE) // 글이 삭제되면 댓글 모두 삭제
  @OrderBy(&quot;createdDate&quot;)
  private List&lt;Reply&gt; replies = new ArrayList&lt;&gt;();


  public void setUser(User user){ // 연관관계 편의 메서드
    this.user = user;
  }

  public void update(BoardUpdateRequestDto request) {
    this.title = request.getTitle();
    this.content = request.getContent();
  }
}</code></pre>
<ul>
<li>게시물 삭제 시 댓글도 함께 삭데되도록 cascade 설정함.</li>
<li>게시물 가져올 땐 댓글도 필수이니 EAGER로 설정했음.. <del>하지만 나중에 문제가 될지 몰랐지..</del></li>
</ul>
<hr>
<h2 id="replyrepository">ReplyRepository</h2>
<pre><code class="language-java">public interface ReplyRepository extends JpaRepository&lt;Reply, Long&gt; {
  Page&lt;Reply&gt; findAll(Pageable pageable);
}</code></pre>
<h2 id="replyservice">ReplyService</h2>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ReplyService {
  private final ReplyRepository replyRepository;
  private final BoardRepository boardRepository;
  private final EntityManager em;

  // 댓글 등록
  public void save(Long boardId, User user, ReplySaveRequestDto replySaveRequestDto){
    Board board = boardRepository.findById(boardId)
        .orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 포스트입니다.&quot;)); // 이러면 코드가 너무 반복되지 않나..음..방법을 찾아보자..
    Reply reply = replySaveRequestDto.toEntity();
    reply.setBoard(board);
    reply.setUser(user);
    replyRepository.save(reply);
  }
  // 댓글 조회 (전체) , 댓글 단건 조회는 딱히,, &quot;내가 쓴 댓글 보기&quot;에서 사용할 메서드
  // 마이페이지 구현할 때 구현할 것
  // 댓글 삭제
  public Long delete(Long id){
    // 여기서 postID를 반환하게 ..
    Long boardId = em.createQuery(&quot;select r.board.id from Reply r where r.id=:id &quot;, Long.class)
        .setParameter(&quot;id&quot;, id)
        .getSingleResult();
    replyRepository.deleteById(id);
    return boardId;
  }
  // 댓글 수정
  @Transactional
  public Long update(Long id, ReplyUpdateRequestDto updateRequestDto){
    Long boardId = em.createQuery(&quot;select r.board.id from Reply r where r.id=:id &quot;, Long.class)
        .setParameter(&quot;id&quot;, id)
        .getSingleResult();
    Reply reply = replyRepository.findById(id).orElseThrow(IllegalArgumentException::new);
    reply.update(updateRequestDto);
    return boardId;
  }
}</code></pre>
<h2 id="replyviewcontroller">ReplyViewController</h2>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
public class ReplyViewController {
  private final ReplyService replyService;
  private final BoardService boardService;

  // 댓글 등록
  @PostMapping(&quot;/boards/reply/{boardId}&quot;)
  public String save(@PathVariable Long boardId, @ModelAttribute ReplySaveRequestDto replySaveRequestDto,
                     HttpSession session) {
    User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
    replyService.save(boardId, loginUser, replySaveRequestDto);
    return &quot;redirect:/boards/&quot;+boardId;
  }
  // 댓글 삭제
  @DeleteMapping(&quot;/boards/reply/{replyId}&quot;)
  public String delete(@PathVariable Long replyId) {
    // 게시판의 아이디가 아니라 댓글의 아이디 여야함.
    // board로 어떻게 보내지,, -&gt; replyId를 사용해서 쿼리문 조인해서 보드 아이디 가져오게함. 지금은 생각나는 방법이 이거밖에 없음..
    Long boardId = replyService.delete(replyId);
    return &quot;redirect:/boards/&quot;+boardId;
  }

  // 댓글 수정
  @PostMapping(&quot;/boards/edit/reply/{replyId}&quot;)
  public String update(@PathVariable Long replyId, @ModelAttribute ReplyUpdateRequestDto updateRequestDto){
    Long boardId = replyService.update(replyId, updateRequestDto);
    return &quot;redirect:/boards/&quot;+boardId;
  }
  // 댓글 조회 - &quot;내가 쓴 댓글 보기&quot;를 클릭한 경우

}</code></pre>
<h2 id="thymeleaf---detailboardhtml">Thymeleaf - detailBoard.html</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;상세 조회 화면&lt;/title&gt;
  &lt;link rel=&quot;stylesheet&quot; th:href=&quot;@{css/board/detailBoard}&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;board-body&quot;&gt;
      작성자 : &lt;span th:text=&quot;${board.nickname}&quot;&gt;&lt;/span&gt;
      &lt;h4 class=&quot;title&quot; th:text=&quot;${board.title}&quot;&gt;&lt;/h4&gt;
      &lt;p class=&quot;content&quot; th:text=&quot;${board.content}&quot;&gt;&lt;/p&gt; &lt;!-- 나중에 텍스트....처리하는 css할려고함.--&gt;
      &lt;!--태그--&gt;
      &lt;div class=&quot;tags&quot; th:each=&quot;tag : ${board.getTags()}&quot;&gt;
        &lt;span th:text=&quot;${tag.tagName}&quot;&gt;&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;!--버튼 영역--&gt;
    &lt;div&gt;
      &lt;form th:action=&quot;@{/boards/{boardId}(boardId=${board.id})}&quot; th:method=&quot;delete&quot;&gt;
        &lt;button type=&quot;button&quot;
                th:onclick=&quot;|location.href=&#39;@{/boards}&#39;|&quot;&gt;취소&lt;/button&gt;
        &lt;button type=&quot;button&quot;
                th:if=&quot;${board.nickname != null &amp;&amp; board.nickname == loginUser.nickname}&quot;
                th:onclick=&quot;|location.href=&#39;@{/boards/{boardId}/edit(boardId=${board.id})}&#39;|&quot;&gt;수정&lt;/button&gt;
        &lt;button type=&quot;submit&quot;
                th:if=&quot;${board.nickname != null &amp;&amp; board.nickname == loginUser.nickname}&quot;
                th:onclick=&quot;|location.href=&#39;@{/boards/{boardId}(boardId=${board.id})}&#39;|&quot;&gt;삭제&lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
    &lt;hr&gt;
    &lt;!-- 댓글 영역 --&gt;
    &lt;div class=&quot;reply-container&quot;&gt;
      &lt;!-- 댓글 입력 폼 --&gt;
      &lt;form th:action=&quot;@{/boards/reply/{boardId}(boardId=${board.id})}&quot; th:object=&quot;${replySaveRequestDto}&quot; method=&quot;post&quot;&gt;
        &lt;textarea rows=&quot;5&quot; cols=&quot;50&quot; th:field=&quot;*{comment}&quot;&gt;&lt;/textarea&gt;
        &lt;button type=&quot;submit&quot;&gt;등록&lt;/button&gt;
      &lt;/form&gt;

      &lt;div class=&quot;reply&quot; th:each=&quot;reply :${board.getReplies()} &quot;&gt;
        &lt;!-- 댓글 수정 폼 --&gt;
        &lt;form class=&quot;edit-form&quot; style=&quot;display: none&quot;
              th:action=&quot;@{/boards/edit/reply/{replyId}(replyId=${reply.id})}&quot;
              th:object=&quot;${replyUpdateRequestDto}&quot; method=&quot;post&quot;&gt;
          &lt;textarea name=&quot;editedContent&quot; rows=&quot;5&quot; cols=&quot;50&quot;
                    th:field=&quot;*{comment}&quot; placeholder=&quot;수정할 댓글을 입력해주세요&quot;&gt;&lt;/textarea&gt;
          &lt;button type=&quot;submit&quot;&gt;저장&lt;/button&gt;
        &lt;/form&gt;

        &lt;p th:text=&quot;${reply.comment}&quot;&gt;&lt;/p&gt;
        &lt;p th:text=&quot;${reply.nickname}&quot;&gt;&lt;/p&gt;
        &lt;p th:text=&quot;${reply.lastModifiedDate}&quot;&gt;&lt;/p&gt;
        &lt;form th:action=&quot;@{/boards/reply/{replyId}(replyId=${reply.id})}&quot; th:method=&quot;delete&quot;&gt;
          &lt;button
              th:if=&quot;${reply.nickname != null &amp;&amp; reply.nickname == loginUser.nickname}&quot;
              type=&quot;submit&quot;&gt;삭제
          &lt;/button&gt;
          &lt;button
              th:if=&quot;${reply.nickname != null &amp;&amp; reply.nickname == loginUser.nickname}&quot;
              th:onclick=&quot;&#39;toggleEditForm(this)&#39;&quot;
              type=&quot;button&quot;&gt;수정
          &lt;/button&gt;
        &lt;/form&gt;
        &lt;hr&gt;
      &lt;/div&gt;
    &lt;/div&gt;&lt;!-- reply-container --&gt;

  &lt;/div&gt;&lt;!--container --&gt;
  &lt;script th:inline=&quot;javascript&quot;&gt;
      function toggleEditForm(button) {
          var replyContainer = button.closest(&#39;.reply&#39;);
          var editForm = replyContainer.querySelector(&#39;.edit-form&#39;);

          if (editForm.style.display == &#39;none&#39;) {
              editForm.style.display = &#39;block&#39;;
          } else {
              editForm.style.display = &#39;none&#39;;
          }
      }
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h2 id="동작-화면">동작 화면</h2>
<p><img src="https://velog.velcdn.com/images/security-won/post/29841aab-2cac-4e25-bf8b-eebe7386c544/image.gif" alt=""></p>
<p>대댓글 기능도 추가할기 말지 고민이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 개발일지 (3) 자유게시판  글 CRUD + 페이징]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%9E%90%EC%9C%A0%EA%B2%8C%EC%8B%9C%ED%8C%90-3</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%9E%90%EC%9C%A0%EA%B2%8C%EC%8B%9C%ED%8C%90-3</guid>
            <pubDate>Thu, 12 Oct 2023 18:24:21 GMT</pubDate>
            <description><![CDATA[<p>자유 게시판 기능부터 구현해보도록 하겠다.
<del>상대적으로 ,, 간단해보인다,,,</del></p>
<p>우선 값 검증이나 이런것들은 한번에 하겠다.</p>
<p>참고로 제대로 동작하는지 위해 다음과 같이 임시 데이터를 넣어줬다.
(@PostConstruct 사용)
<img src="https://velog.velcdn.com/images/security-won/post/7ee76163-f858-45fd-b7dc-49de55819de5/image.png" alt=""></p>
<h2 id="basetimeentity">baseTimeEntity</h2>
<pre><code class="language-java">@MappedSuperclass
@Getter
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

  @CreatedDate
  @Column(updatable = false)
  private LocalDateTime createdDate; // 등록일

  @LastModifiedDate
  private LocalDateTime lastModifiedDate; // 수정일

  @PrePersist
  public void prePersist() {
    LocalDateTime now = LocalDateTime.now();
    createdDate = now;
    lastModifiedDate = now;
  }
  @PreUpdate
  public void preUpdate(){
    LocalDateTime now = LocalDateTime.now();
    lastModifiedDate = now;
  }![](https://velog.velcdn.com/images/security-won/post/ffc89259-97f5-4f89-9789-e65f007cfa8f/image.gif)


}</code></pre>
<h2 id="user">User</h2>
<pre><code class="language-java">@Entity @Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = &quot;users&quot;)
public class User extends BaseTimeEntity {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = &quot;user_id&quot;)
  private Long id;
  private String name; // 본명
  private String nickname; // 별명
  private String password;
  private String email;

  @Builder
  public User(String name, String nickname, String password, String email) {
    this.name = name;
    this.nickname = nickname;
    this.password = password;
    this.email = email;
  }

  public void update(UserUpdateRequestDto updateDto) {
    // 갑자기 궁금한게 회원정보를 닉네임만 변경하던가 비번만 변경할 수 있는데 이럴경우 업데이트 메서드를 따로 분리해야하나?
    this.nickname = updateDto.getNickname();
    this.password = updateDto.getPassword();
  }
}</code></pre>
<h2 id="board">board</h2>
<ul>
<li><p>자유게시판 엔티티</p>
<pre><code class="language-java">@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends BaseTimeEntity {

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

private String title;
@Lob
private String content;

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = &quot;user_id&quot;)
private User user;

public void setUser(User user){ 
  this.user = user;
}

public void update(BoardUpdateRequestDto request) {
  this.title = request.getTitle();
  this.content = request.getContent();
}
}</code></pre>
</li>
<li><p>User와 board는 일대다 관계. </p>
<h2 id="boardrepository">boardRepository</h2>
<pre><code class="language-java">@Repository
public interface BoardRepository extends JpaRepository&lt;Board, Long&gt; {

Page&lt;Board&gt; findAll(Pageable pageable);
}</code></pre>
<h2 id="boardservice">boardService</h2>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;

// 게시판 등록
public Long save(BoardSaveRequestDto request, User user){
  Board board = request.toEntity();
  board.setUser(user);
  boardRepository.save(board);
  return board.getId();
}
// 게시판 조회 - 단건
public BoardResponseDto findById(Long id){
  Board board = boardRepository.findById(id).orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 포스트입니다.&quot;));
  return new BoardResponseDto(board);
}
// 게시판 조회 - 전체
public List&lt;BoardResponseDto&gt; findAll() {
  List&lt;BoardResponseDto&gt; boards = boardRepository.findAll()
      .stream()
      .map(BoardResponseDto::new)
      .collect(Collectors.toList());
  return boards;
}
// 게시판 조회 - 전체, 페이징 적용
public Page&lt;BoardResponseDto&gt; findAll(Pageable pageable) {
  Page&lt;BoardResponseDto&gt; boards = boardRepository.findAll(pageable).map(BoardResponseDto::new);
  return boards;
}
// 회원 삭제
public void delete(Long id){
  boardRepository.deleteById(id);
}

// 게시판 수정
@Transactional
public void update(Long id, BoardUpdateRequestDto request){
  Board board = boardRepository.findById(id)
      .orElseThrow(() -&gt; new IllegalArgumentException(&quot;존재하지 않는 포스트입니다.&quot;));
  board.update(request);
}
}</code></pre>
<h2 id="boardviewcontroller">boardViewController</h2>
<pre><code class="language-java">package com.jiwon.blog.web.controller.view;
</code></pre>
</li>
</ul>
<p>import com.jiwon.blog.domain.user.User;
import com.jiwon.blog.repository.UserRepository;
import com.jiwon.blog.service.BoardService;
import com.jiwon.blog.web.dto.board.BoardResponseDto;
import com.jiwon.blog.web.dto.board.BoardSaveRequestDto;
import com.jiwon.blog.web.dto.board.BoardUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;</p>
<p>import javax.servlet.http.HttpSession;
import java.util.List;</p>
<p>@Controller
@RequiredArgsConstructor
public class BoardViewController {</p>
<p>  private final BoardService boardService;</p>
<p>  // 글 등록 폼 보여주기
  @GetMapping(&quot;/board/form&quot;)
  public String boardWrite(Model model){
    model.addAttribute(&quot;boardSaveRequestDto&quot;, new BoardSaveRequestDto());
    return &quot;board/form&quot;;
  }</p>
<p>  // 글 등록
  @PostMapping(&quot;/board/form&quot;)
  public String boardSave(@ModelAttribute BoardSaveRequestDto boardSaveRequestDto, HttpSession session){
    User loginUser = (User) session.getAttribute(&quot;loginUser&quot;);
    boardService.save(boardSaveRequestDto, loginUser);
    return  &quot;redirect:/boards&quot;;
  }</p>
<p>  // 글 조회 - 단건 = 상세 조회
  @GetMapping(&quot;/boards/{id}&quot;)
  public String boards(@PathVariable Long id, Model model) {
    BoardResponseDto board = boardService.findById(id);
    model.addAttribute(&quot;board&quot;, board);
    return &quot;board/detailBoard&quot;;</p>
<p>  }
  // 글 조회 - 전체 - 페이징
  @GetMapping(&quot;/boards&quot;)
  public String boards(Model model,
                       @PageableDefault(size = 5, sort = &quot;createdDate&quot;,
                           direction = Sort.Direction.DESC) Pageable pageable) {
    Page<BoardResponseDto> boards = boardService.findAll(pageable);
    model.addAttribute(&quot;boards&quot;, boards);
    return &quot;board/main&quot;;</p>
<p>  }
  // 글 삭제
  @DeleteMapping(&quot;/boards/{id}&quot;)
  public String delete(@PathVariable Long id){
    boardService.delete(id);
    return &quot;redirect:/boards&quot;;
  }</p>
<p>  // 글 수정 폼
  @GetMapping(&quot;/boards/{id}/edit&quot;)
  public String updateForm(@PathVariable Long id, Model model){
    BoardResponseDto board = boardService.findById(id);
    BoardUpdateRequestDto boardUpdateRequestDto = new BoardUpdateRequestDto();
    boardUpdateRequestDto.setTitle(board.getTitle());
    boardUpdateRequestDto.setContent(board.getContent());</p>
<pre><code>model.addAttribute(&quot;boardUpdateRequestDto&quot;, new BoardUpdateRequestDto());
return &quot;board/modifyForm&quot;;</code></pre><p>  }
  // 글 수정
  @PutMapping(&quot;/boards/{id}/edit&quot;)
  public String update(@PathVariable Long id,
                       @ModelAttribute BoardUpdateRequestDto boardUpdateRequestDto,
                       @RequestParam(defaultValue = &quot;/&quot;) String redirectURL){
    boardService.update(id, boardUpdateRequestDto);
    return &quot;redirect:/boards/&quot;+id;
  }
}</p>
<pre><code>## 
---
- CRUD 기능 구현, 임시지만 로그인 한 사용자가 글 작성시 글과 작성자가 제댈 매핑되는지 확인하기 위해 아주 간단한 로그인 로직을 구성했다.
- 그리고 delete와 put 메서드르 사용하기 위해 application.yml에 다음과 같이 설정해줌으로써 put, delete 메서드를 form태그에서 사용할 수 있게되었다.
- 참고로 th:method=&quot;delete&quot;, th:method=&quot;put&quot;이라고 작성해야한다. th를 써주지 않으면 제대로 동작이 안한다.</code></pre><p>spring:
 ... 
  mvc:
    hiddenmethod:
      filter:
        enabled: true</p>
<pre><code>
- 아직은 단순히 CRUD 위주로 구현해서 디테일이 부족하다. 내가 쓴 글에만 삭제와 수정 버튼이 보이게하는지 등 이러한 디테일은 계속 개발을 하면서 수정해나갈 것이다.
- 그리고 page 구현했따^^ sw 직무 역량 부트캠프를 수강하면서 페이징 구현을 하지 못했는데 공부하고 직접 구현해보니까 너무 뿌듯하다.
- UI는 나중에....

짱구로 로그인한 후 테스트 시연이다. ~~(맥북 영상녹화 시프트 + 커맨드 + 숫자 5)~~![](https://velog.velcdn.com/images/security-won/post/d9609f99-d094-480f-a59f-8eceb86a1172/image.gif)

디테일보단 우선 기능위주로,,, ~~UI는 참참참^^~~</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 개발일지 (2)]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-2</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%EC%B7%A8%EB%AF%B8-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-2</guid>
            <pubDate>Wed, 11 Oct 2023 12:03:11 GMT</pubDate>
            <description><![CDATA[<h2 id="user">User</h2>
<pre><code class="language-java">@Entity @Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = &quot;users&quot;)
public class User extends BaseTimeEntity {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String username; // 본명
  private String nickname; // 별명
  private String password;
  private String email;

  @OneToMany(mappedBy = &quot;user&quot;)
  private Set&lt;UserTag&gt; tags = new HashSet&lt;&gt;(); // tag를 중복으로 가질 수 없으니 Set 자료형으로 함.

  @Builder
  public User(String username, String nickname, String password, String email) {
    this.username = username;
    this.nickname = nickname;
    this.password = password;
    this.email = email;
  }

  public void update(UserUpdateRequestDto updateDto) {
    this.nickname = updateDto.getNickname();
    this.password = updateDto.getPassword();
  }
}</code></pre>
<ul>
<li><p>BaseTimeEntity는 등록일과 수정일이 들어있는 베이스 엔티티</p>
</li>
<li><p>User와 Tag는 다대다 관계로 설정함.</p>
</li>
<li><p>다대다 관계는 @ManyToMany어노테이션을 사용해서 구현할 수 있지만 다대다 관계를 일대다,다대일 관계로 풀어내서 작성하는 것이 더 좋아서 UserTag라는 중간 테이블을 생성함</p>
</li>
<li><p>(프로필 기능은 후에 추가할 것)</p>
<h2 id="tag">Tag</h2>
<pre><code class="language-java">@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED)
@Getter @Entity @Builder
public class Tag extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String tagName;
}</code></pre>
<h2 id="usertag">UserTag</h2>
<pre><code class="language-java">@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Getter @Entity @Builder
public class UserTag {
// user - UserTag - tag
// User는 관심 주제를 Tag로 설정해서 알림을 받을 수 있도록함.
// 그래서 user와 tag는 다대다 관계여서 일대다, 다대일 관계로 풀어주기 위한 중간 테이블 생성
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne
@JoinColumn(name = &quot;user_id&quot;)
private User user;

@ManyToOne
@JoinColumn(name=&quot;tag_id&quot;)
private Tag tag;
}</code></pre>
<h2 id="crew">Crew</h2>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Crew extends BaseTimeEntity {

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

private Long leaderId; // 크루장 id
private String name; // 크루이름
private boolean type; //  true-온라인, false-오프라인
private boolean cost; // true-유료탑승, false-무료탑승
private String thumbnail; //이미지 경로

@Lob // varchar보다 클 경우 사용하는 어노테이션
private String description; // 크루즈 설명
private byte capacity; // 인원수

@OneToMany(mappedBy = &quot;crew&quot;)
private Set&lt;CrewTag&gt; tags = new HashSet&lt;&gt;(); // tag를 중복으로 가질 수 없으니 Set 자료형으로 함.


</code></pre>
</li>
</ul>
<p>  @Builder
  public Crew(Long leaderId, String name, boolean type, boolean cost,
              String introduction, byte capacity, String thumbnail) {
    this.leaderId = leaderId;
    this.name = name;
    this.type = type;
    this.cost = cost;
    this.description = introduction;
    this.capacity = capacity;
    this.thumbnail = thumbnail;
  }
  public void update(CrewUpdateRequestDto updateDto) {
    this.name = updateDto.getName();
    this.type = updateDto.isType();
    this.cost = updateDto.isCost();
    this.introduction = updateDto.getIntroduction();
    this.capacity = updateDto.getCapacity();
    this.thumbnail = updateDto.getThumbnail();
    }
}</p>
<pre><code>- 갑자기 코드가 와다다다닥 있어서 당황스럽겠지만 개발하면서 필요할꺼같은 메서드들은 미리 생성해두었다.
## CrewTag
```java
@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED)
@Getter @Entity @Builder
public class CrewTag {
  // Crew-CrewTag-tag , 다대다 관계를 일대다 다대일 관계로 풀어내기 위한 중간 테이블
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = &quot;crew_id&quot;)
  private Crew crew;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name=&quot;tag_id&quot;)
  private Tag tag;
}</code></pre><p>뭔가 빵꾸가 와장창 있는 코드같다.. 공부하면서 리팩토링을 계속 해야겠다.
음...우선 도메인을 구성하면서 든 생각이,, 자유게시판부터 구현해야겠다.
다음 포스팅은 자유게시판 구현부터 시작하겠다.</p>
<hr>
<p>조언, 충고,,잔소리 쓴소리 너무 감사합니다.</p>
<hr>
<p>10.16
양방향 관계가 아니라 단방향 관계로 수정함.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Data jpa 페이징 (2)]]></title>
            <link>https://velog.io/@security-won/Spring-Data-jpa-%ED%8E%98%EC%9D%B4%EC%A7%95-2</link>
            <guid>https://velog.io/@security-won/Spring-Data-jpa-%ED%8E%98%EC%9D%B4%EC%A7%95-2</guid>
            <pubDate>Tue, 10 Oct 2023 12:17:53 GMT</pubDate>
            <description><![CDATA[<p>Spring Data Jpa에서 페이징과 정렬기능을 스프링 MVC에서 편하게 사용할 수 있다.</p>
<p>바로 코드를 봐보자.</p>
<hr>
<h3 id="controller">Controller</h3>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class MemberController {
  private final MemberRepository memberRepository;

  @GetMapping(&quot;/members&quot;)
  public Page&lt;Member&gt; memberList(Pageable pageable){
    Page&lt;Member&gt; page = memberRepository.findAll(pageable);
    return page;
  }

  @PostConstruct
  public void init() {
    for(int i = 0; i&lt;100; i++){
      memberRepository.save(new Member(&quot;user&quot;+i));
    }
  }
}</code></pre>
<ul>
<li>memberList 메서드의 파리미터로 Pageable이 날라온다.</li>
<li>pageable은 인터페이스여서 구현체로 PageRequest를 사용한다.<pre><code class="language-java">protected PageRequest(int page, int size, Sort sort) {
  super(page, size);
  Assert.notNull(sort, &quot;Sort must not be null&quot;);
  this.sort = sort;
}
public static PageRequest of(int page, int size) {
  eturn of(page, size, Sort.unsorted());
}
</code></pre>
</li>
</ul>
<p>public static PageRequest of(int page, int size, Sort sort) {
    return new PageRequest(page, size, sort);
}</p>
<p>public static PageRequest of(int page, int size, Direction direction, String... properties) {
    return of(page, size, Sort.by(direction, properties));
}</p>
<pre><code>- PageRequest를 생성할 때 생성자를 통해 객체를 생성하는 것은 deprecated되어 있다고 한다. 그래서 정적 팩토리 메서드 패턴으로 PageRequest.of(...)로 PageRequest를 생성한다.
- 다시 본론으로 돌아가자면 파라미터로 Pageable을 받아 Web의 요청 파라미터를 Pageable과 바인딩할 수 있다.
- 정리하자면 컨트롤러에서 파라미터로 Pageable이 있으면 스프링 부트가 PageRequest객체를 생성해서 파라미터로 넘어온 값들을 바인딩 해준다.

---
이렇게 작성하면 끝이다.
postman을 통해 페이징 파라미터를 함께 전달해보자

### 요청 파라미터
- page : 현재 페이지, 0부터 시작
- size : 한 페이지에 노출할 데이터 건수
- sort : 정렬 조건

### 요청 파라미터 page</code></pre><p>GET - <a href="http://localhost:8080/members?page=0">http://localhost:8080/members?page=0</a></p>
<pre><code>### 결과
```json
{
    &quot;content&quot;: [
        {
            &quot;createdDate&quot;: &quot;2023-10-10T12:42:35.991973&quot;,
            &quot;updatedDate&quot;: &quot;2023-10-10T12:42:35.991973&quot;,
            &quot;id&quot;: 1,
            &quot;username&quot;: &quot;user0&quot;,
            &quot;age&quot;: 0,
            &quot;team&quot;: null
        },
        {
            &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.021407&quot;,
            &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.021407&quot;,
            &quot;id&quot;: 2,
            &quot;username&quot;: &quot;user1&quot;,
            &quot;age&quot;: 1,
            &quot;team&quot;: null
        },

        ...

        {
            &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.06707&quot;,
            &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.06707&quot;,
            &quot;id&quot;: 18,
            &quot;username&quot;: &quot;user17&quot;,
            &quot;age&quot;: 17,
            &quot;team&quot;: null
        },
        {
            &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.068257&quot;,
            &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.068257&quot;,
            &quot;id&quot;: 19,
            &quot;username&quot;: &quot;user18&quot;,
            &quot;age&quot;: 18,
            &quot;team&quot;: null
        },
        {
            &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.069959&quot;,
            &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.069959&quot;,
            &quot;id&quot;: 20,
            &quot;username&quot;: &quot;user19&quot;,
            &quot;age&quot;: 19,
            &quot;team&quot;: null
        }
    ],
    &quot;pageable&quot;: {
        &quot;sort&quot;: {
            &quot;empty&quot;: true,
            &quot;unsorted&quot;: true,
            &quot;sorted&quot;: false
        },
        &quot;offset&quot;: 0,
        &quot;pageNumber&quot;: 0,
        &quot;pageSize&quot;: 20,
        &quot;paged&quot;: true,
        &quot;unpaged&quot;: false
    },
    &quot;last&quot;: false,
    &quot;totalElements&quot;: 100,
    &quot;totalPages&quot;: 5,
    &quot;number&quot;: 0,
    &quot;first&quot;: true,
    &quot;sort&quot;: {
        &quot;empty&quot;: true,
        &quot;unsorted&quot;: true,
        &quot;sorted&quot;: false
    },
    &quot;size&quot;: 20,
    &quot;numberOfElements&quot;: 20,
    &quot;empty&quot;: false
}</code></pre><ul>
<li>결과를 보니 디폴트가 20개인가보다. 20개가 불러와졌다.<h3 id="요청-파라미터-page--size">요청 파라미터 page &amp; size</h3>
<pre><code>GET - http://localhost:8080/members?page=0&amp;size=3</code></pre><h3 id="결과">결과</h3>
<pre><code class="language-json">{
  &quot;content&quot;: [
      {
          &quot;createdDate&quot;: &quot;2023-10-10T12:42:35.991973&quot;,
          &quot;updatedDate&quot;: &quot;2023-10-10T12:42:35.991973&quot;,
          &quot;id&quot;: 1,
          &quot;username&quot;: &quot;user0&quot;,
          &quot;age&quot;: 0,
          &quot;team&quot;: null
      },
      {
          &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.021407&quot;,
          &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.021407&quot;,
          &quot;id&quot;: 2,
          &quot;username&quot;: &quot;user1&quot;,
          &quot;age&quot;: 1,
          &quot;team&quot;: null
      }
  ],
  &quot;pageable&quot;: {
      &quot;sort&quot;: {
          &quot;empty&quot;: true,
          &quot;unsorted&quot;: true,
          &quot;sorted&quot;: false
      },
      &quot;offset&quot;: 0,
      &quot;pageNumber&quot;: 0,
      &quot;pageSize&quot;: 2,
      &quot;paged&quot;: true,
      &quot;unpaged&quot;: false
  },
  &quot;last&quot;: false,
  &quot;totalElements&quot;: 100,
  &quot;totalPages&quot;: 50,
  &quot;number&quot;: 0,
  &quot;first&quot;: true,
  &quot;sort&quot;: {
      &quot;empty&quot;: true,
      &quot;unsorted&quot;: true,
      &quot;sorted&quot;: false
  },
  &quot;size&quot;: 2,
  &quot;numberOfElements&quot;: 2,
  &quot;empty&quot;: false
}
</code></pre>
</li>
</ul>
<pre><code>- page파라미터를 1로 바꾸면 2번째 페이지로 , 3과 4를 가져온다. 3으로 바꾸면 5와6을 가져올 것이다
```json
{
    &quot;content&quot;: [
        {
            &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.025348&quot;,
            &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.025348&quot;,
            &quot;id&quot;: 3,
            &quot;username&quot;: &quot;user2&quot;,
            &quot;age&quot;: 2,
            &quot;team&quot;: null
        },
        {
            &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.04167&quot;,
            &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.04167&quot;,
            &quot;id&quot;: 4,
            &quot;username&quot;: &quot;user3&quot;,
            &quot;age&quot;: 3,
            &quot;team&quot;: null
        }
    ],
    &quot;pageable&quot;: {
        &quot;sort&quot;: {
            &quot;empty&quot;: true,
            &quot;unsorted&quot;: true,
            &quot;sorted&quot;: false
        },
        &quot;offset&quot;: 2,
        &quot;pageNumber&quot;: 1,
        &quot;pageSize&quot;: 2,
        &quot;paged&quot;: true,
        &quot;unpaged&quot;: false
    },
    &quot;last&quot;: false,
    &quot;totalElements&quot;: 100,
    &quot;totalPages&quot;: 50,
    &quot;number&quot;: 1,
    &quot;first&quot;: false,
    &quot;sort&quot;: {
        &quot;empty&quot;: true,
        &quot;unsorted&quot;: true,
        &quot;sorted&quot;: false
    },
    &quot;size&quot;: 2,
    &quot;numberOfElements&quot;: 2,
    &quot;empty&quot;: false
}</code></pre><p>sort를 추가해보자
오름차순은 결과과 위와 똑같아서 내림차순으로 했다.</p>
<h3 id="요청-파라미터-page--size--sort">요청 파라미터 page &amp; size &amp; sort</h3>
<pre><code>GET - http://localhost:8080/members?page=0&amp;size=2&amp;sort=id,desc</code></pre><ul>
<li>참고로 Sort조건은 &amp;를 사용해서 여러 개 넣어줄 수 있다.
ex) <a href="http://localhost:8080/members?page=0&amp;size=2&amp;sort=id,desc&amp;sort=username,desc">http://localhost:8080/members?page=0&amp;size=2&amp;sort=id,desc&amp;sort=username,desc</a><h3 id="결과-1">결과</h3>
<pre><code class="language-json">{
  &quot;content&quot;: [
      {
          &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.15968&quot;,
          &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.15968&quot;,
          &quot;id&quot;: 100,
          &quot;username&quot;: &quot;user99&quot;,
          &quot;age&quot;: 99,
          &quot;team&quot;: null
      },
      {
          &quot;createdDate&quot;: &quot;2023-10-10T12:42:36.158977&quot;,
          &quot;updatedDate&quot;: &quot;2023-10-10T12:42:36.158977&quot;,
          &quot;id&quot;: 99,
          &quot;username&quot;: &quot;user98&quot;,
          &quot;age&quot;: 98,
          &quot;team&quot;: null
      }
  ],
  &quot;pageable&quot;: {
      &quot;sort&quot;: {
          &quot;empty&quot;: false,
          &quot;unsorted&quot;: false,
          &quot;sorted&quot;: true
      },
      &quot;offset&quot;: 0,
      &quot;pageNumber&quot;: 0,
      &quot;pageSize&quot;: 2,
      &quot;paged&quot;: true,
      &quot;unpaged&quot;: false
  },
  &quot;last&quot;: false,
  &quot;totalElements&quot;: 100,
  &quot;totalPages&quot;: 50,
  &quot;number&quot;: 0,
  &quot;first&quot;: true,
  &quot;sort&quot;: {
      &quot;empty&quot;: false,
      &quot;unsorted&quot;: false,
      &quot;sorted&quot;: true
  },
  &quot;size&quot;: 2,
  &quot;numberOfElements&quot;: 2,
  &quot;empty&quot;: false
}
</code></pre>
</li>
</ul>
<pre><code>
### DTO 변환
API 요청 결고값으로 엔티티를 그대로 노출시키는 것보다 DTO로 변환해서 노출시켜야한다.
Page는 map()지원해서 쉽게 DTO로 변환할 수 있다.
```java
@GetMapping(&quot;/members&quot;) 
public Page&lt;MemberDto&gt; list(Pageable pageable) { 
    Page&lt;Member&gt; page = memberRepository.findAll(pageable);
    Page&lt;MemberDto&gt; pageDto = page.map(MemberDto::new);
    return pageDto;
}</code></pre><p>인프런 - (실전! 스프링 데이터 JPA)[<a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard%5D%EB%A5%BC">https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard]를</a> 수강하면서 정리한 글입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[개발일지] 취미 커뮤니티 개발일지 (1)]]></title>
            <link>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-blog-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-1</link>
            <guid>https://velog.io/@security-won/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-blog-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-1</guid>
            <pubDate>Tue, 10 Oct 2023 09:06:13 GMT</pubDate>
            <description><![CDATA[<h2 id="설정">설정</h2>
<ul>
<li>spring boot 2.7.16</li>
<li>java 11</li>
<li>H2 DB</li>
<li>Spring Data jpa</li>
<li>thymeleaf</li>
<li>기능 구현하면서 추가할 예정</li>
</ul>
<p>이번에 만들어볼 프로젝트는 취미 생활을 함께 할 사람들을 모집 또는 참여할 수 있는 커뮤니티 서비스를 만들어 볼려고 한다.
구현해보고 싶은 기능들이 산더미지만 천천히 하나하나 해볼 생각이다.</p>
<p>먼저...</p>
<h2 id="요구사항-분석-및-필요-기능">요구사항 분석 및 필요 기능</h2>
<p>모든 기능이 구현이 가능할지는 모르겠지만 우선 구현해보고싶은 기능들을 작성해나가고 하나하나 공부해가면서 구현할 생각이다.</p>
<p>👍 회원</p>
<ul>
<li>회원 가입</li>
<li>로그인 및 로그아웃</li>
<li>로그인 유지</li>
<li>프로필 </li>
<li>회원 CRUD</li>
<li>회원 탈퇴</li>
<li>비밀번호 수정</li>
<li>OAuth2.0 구현</li>
</ul>
<p>👍 크루</p>
<ul>
<li>크루 모집 글 CRUD </li>
<li>크루 참가 및 탈퇴</li>
<li>크루 관심있어요(좋아요) 기능</li>
<li>크루 공개, 크루원 모집, 크루 활동 종료 등 각종 설정들</li>
</ul>
<p>👍 자유게시판</p>
<ul>
<li>게시글 CRUD</li>
<li>댓글 및 대댓글 기능</li>
</ul>
<p>👍 기타</p>
<ul>
<li>게시글 검색</li>
<li>웹소켓 적용한 기능</li>
<li>예외 처리 및 검증</li>
<li>페이징 적용</li>
<li>등등등</li>
</ul>
<p>등등 구현해보고 싶은 기능들이다....
열심히.....</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 페이징]]></title>
            <link>https://velog.io/@security-won/JPA-%ED%8E%98%EC%9D%B4%EC%A7%95</link>
            <guid>https://velog.io/@security-won/JPA-%ED%8E%98%EC%9D%B4%EC%A7%95</guid>
            <pubDate>Sun, 08 Oct 2023 12:57:02 GMT</pubDate>
            <description><![CDATA[<p>SW 직무 역량 부트캠프를 수강했을 때 진행한 팀 프로젝트에서 페이징 기능 구현에 실패했다.
그래서 페이징에 대한 내용을 진짜 공부하고싶었는데 공부한 부분을 정리하겠다.</p>
<h2 id="jpa-페이징-코드">JPA 페이징 코드</h2>
<pre><code class="language-java">public List&lt;Member&gt; findByPage(int age, int offset, int limit){
    return em.createQuery(&quot;select m from Member m where m.age = : age order by m.username desc&quot;, Member.class)
        .setParameter(&quot;age&quot;, age)
        .setFirstResult(offset) // 어디서부터 가져올꺼야?
        .setMaxResults(limit) // 개수는 몇개 가져올꺼야?
        .getResultList();
  }</code></pre>
<ul>
<li><p>현재 몇 번째 페이지인지를 알기 위한 코드</p>
<pre><code class="language-java">// 현재 몇 번째 페이지인지
public long totalCount(int age){
  return em.createQuery(&quot;select count(m) from Member m where m.age = : age&quot;, Long.class)
      .setParameter(&quot;age&quot;, age)
      .getSingleResult();
}</code></pre>
</li>
<li><p>테스트 코드</p>
<pre><code class="language-java">@Test
public void paging() {
  Member m1 = new Member(&quot;member1&quot;, 10);
  Member m2 = new Member(&quot;member2&quot;, 10);
  Member m3 = new Member(&quot;member3&quot;, 10);
  Member m4 = new Member(&quot;member4&quot;, 20);
  Member m5 = new Member(&quot;member5&quot;, 25);

  memberJpaRepository.save(m1);
  memberJpaRepository.save(m2);
  memberJpaRepository.save(m3);
  memberJpaRepository.save(m4);
  memberJpaRepository.save(m5);

  int age = 10;
  int offset = 0;
  int limit = 3;

  List&lt;Member&gt; result = memberJpaRepository.findByPage(age, offset, limit);
  long totalCount = memberJpaRepository.totalCount(age);

  Assertions.assertThat(result.size()).isEqualTo(3);
  Assertions.assertThat(totalCount).isEqualTo(3);
}</code></pre>
</li>
<li><p>사실 좀 빠진 부분이 있는데 totalCount를 활용해서 현재 페이지 수를 구해야하고,, 마지막 페이지가 있는지, 첫번 째 페이지인지 아닌지 등 좀 고려해야할 부분들이 있는데 Spring Data JPA에서는 이러한 부분들을 알아서 계산해준다.</p>
</li>
</ul>
<p>코드를 봐보자.</p>
<hr>
<h2 id="spring-data-jpa-페이징-코드">Spring Data JPA 페이징 코드</h2>
<ul>
<li>페이징을 인터페이스로 표준화 시킴.</li>
</ul>
<p>[Page 예시 코드 - 나이를 기준으로 검색]</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    // pageable에는 쿼리 조건들을 넣으면 됨.
    Page&lt;Member&gt; findByAge(int age, Pageable pageable); 
  }</code></pre>
<pre><code class="language-java">  @Test
  public void paging() {
    Member m1 = new Member(&quot;member1&quot;, 10);
    Member m2 = new Member(&quot;member2&quot;, 10);
    Member m3 = new Member(&quot;member3&quot;, 10);
    Member m4 = new Member(&quot;member4&quot;, 10);
    Member m5 = new Member(&quot;member5&quot;, 10);

    memberRepository.save(m1);
    memberRepository.save(m2);
    memberRepository.save(m3);
    memberRepository.save(m4);
    memberRepository.save(m5);

    // page0에서 3개 가져와, sorting 조건은 username을 기준으로 내림차순이야.
    PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, &quot;username&quot;));

    // pageRequest은 Pagable 인터페이스의 구현체
    Page&lt;Member&gt; page = memberRepository.findByAge(10, pageRequest);

    // 반환타입이 page면 totalCount 쿼리까지 함꼐 날림!
    // 왜냐하면 페이지 계산하기 위해 필요하니까 알아서 날린거임!
    Page&lt;Member&gt; content = page.getContent();

    // totalCount 가져오는 메서드
    long totalElements = page.getTotalElements();

    for (Member member : content) {
      System.out.println(&quot;member = &quot; + member);
    }
    System.out.println(&quot;totalElements = &quot; + totalElements);

    Assertions.assertThat(content.size()).isEqualTo(3);
    Assertions.assertThat(page.getTotalElements()).isEqualTo(5);
    Assertions.assertThat(page.getNumber()).isEqualTo(0);
    Assertions.assertThat(page.getTotalPages()).isEqualTo(2);
    Assertions.assertThat(page.isFirst()).isTrue();
    Assertions.assertThat(page.hasNext()).isTrue();

  }</code></pre>
<ul>
<li>페이징 관련한 코드는 주석으로 설명을 달아놨다.</li>
<li>agable의 인터페이스의 구현체인 PageRequest를 사용한다.</li>
<li>PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지, 두 번째 파라미터는 조회할 데이터 수, 세 번째 파라미터에는 추가로 정렬 조건을 작성할 수 있다.</li>
<li>그리고 반환타입으로 Page&lt;...&gt;, Slice&lt;...&gt;, List&lt;...&gt;가 있다.<ul>
<li>Page&lt;...&gt; : totalCount 쿼리 결과를 포함 + content 쿼리</li>
<li>Slice&lt;...&gt; : totalCount 쿼리 결과 포함X + content 쿼리 + 다음 페이지만 확인 (limit + 1와서 확인함.)</li>
<li>List&lt;...&gt; : totalCount 쿼리 없이 List 쿼리만 나감.</li>
<li>직접 날려보면서 DB로 나가는 쿼리를 눈으로 확인하는 것을 추천한다.</li>
</ul>
</li>
<li>참고로 페이징 시작은 1부터가 아닌 0부터다.</li>
</ul>
<hr>
<p>인프런 - <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84">실전! 스프링 데이터 JPA</a>를 수강하면서 정리한 글입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 값 타입]]></title>
            <link>https://velog.io/@security-won/JPA-%EA%B0%92-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@security-won/JPA-%EA%B0%92-%ED%83%80%EC%9E%85</guid>
            <pubDate>Wed, 04 Oct 2023 07:20:01 GMT</pubDate>
            <description><![CDATA[<p>JPA는 크게 2가지 데이터 타입으로 나눌 수 있다.</p>
<ul>
<li>엔티티 타입<ul>
<li>@Entity로 정의한 객체</li>
<li>데이터가 변해도 PK같은 식별자로 추적이 가능하다.</li>
</ul>
</li>
<li>값 타입<ul>
<li>int, Integer, String처럼 자바 기본 타입이나 객체</li>
<li>데이터가 변하면 식별자가 없어 추적이 불가능하다.</li>
</ul>
</li>
</ul>
<p>값 타입은 또 다음과 같이 분류할 수 있다.</p>
<ul>
<li>기본값 타입<ul>
<li>자바 기본 타입</li>
<li>래퍼 클래스</li>
<li>String</li>
</ul>
</li>
<li>임베디드 타입<ul>
<li>JPA에서 정의해서 사용해야함</li>
</ul>
</li>
<li>컬렉션 값 타입<ul>
<li>JPA에서 정의해서 사용해야함</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/security-won/post/40a45969-6493-4c9a-877a-92485bb015c6/image.png" alt=""></p>
<ul>
<li>타입에 대해 보다 더 자세히 알아보자.</li>
</ul>
<hr>
<h2 id="기본-값-타입">기본 값 타입</h2>
<ul>
<li>String, int 와 같은 타입들</li>
<li>생명주기를 엔티티의 의존한다.<ul>
<li>예시로 Member 삭제 시 이름과 나이가 함께 삭제됨</li>
</ul>
</li>
<li>값 타입은 공유하면 안된다.<ul>
<li>Member1의 이름 필드 변경 시 Member2의 이름 필드가 변경되면 안됨.</li>
</ul>
</li>
</ul>
<h2 id="임베디드-타입">임베디드 타입</h2>
<ul>
<li>새로운 값 타입을 직접 정의</li>
<li>주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.</li>
<li>예를 들어 Member의 집 주소를 저장할 때 Member 클래스에 도시,우편번호 등의 필드를 따로 생성할 수 있지만 그냥 Address타입을 가지게 하면 가독성이 훨 좋을 것이다.</li>
<li>@Embeddable : 값 타입을 정의하는 곳에</li>
<li>@Embedded : 값 타입을 사용하는 곳에</li>
<li>그리고 기본 생성자는 필수.</li>
</ul>
<p>[임베디드 타입 예시]</p>
<pre><code class="language-java">@Embeddable // 임베디드 타입 정의
public class Address {

    private String city;
    private String street;
    private String zipcode;

    public Address() {}
    ...
}</code></pre>
<pre><code class="language-java">@Entity
public class Member extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;MEMBER_ID&quot;)
    private Long id;

    @Column(name=&quot;name&quot;, nullable = false)
    private String username;

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

    // 임베디드 타입 사용
    @Embedded 
    private Address address;
    ...
}</code></pre>
<ul>
<li><p>그런데 만약 Member가 Address타입을 2개 가지고 있다면?</p>
</li>
<li><p>이럴땐 컬럼명이 중복되기 때문에 @AttributeOverrrides를 사용해서 속성을 재정의해야한다.</p>
<pre><code class="language-java">@Entity
public class Member extends BaseEntity {
  ...

  // 임베디드 타입 사용
  @Embedded 
  private Address homeAddress;

  @Embedded 
  @AttributeOverrrides({
        @AttributeOverrride(name=&quot;city&quot;,
        column=@Column(&quot;work_city&quot;)
        @AttributeOverrride(name=&quot;street&quot;,
        column=@Column(&quot;work_street&quot;)
        @AttributeOverrride(name=&quot;zipcode&quot;,
        column=@Column(&quot;work_zipcode&quot;))
  })
  private Address workAddress;
  ...
}</code></pre>
</li>
<li><p>임베디드 타입을 사용하면 응집도도 높아지고 재사용성도 좋아지지만 입베디드 타입을 여러 엔티티에서 공유하면 위험하다.</p>
</li>
<li><p>그래서 임베디드 타입의 값을 복사해서 사용하는 것을 추천한다.</p>
</li>
<li><p>하지만 정의한 임베디드 타입은 객체여서 참조 값에 직접 값을 대입하는 것은 막을 수 없다는 한계가 있다.</p>
</li>
<li><p>그래서! <strong>객체 타입을 수정할 수 없도록 생성자로만 값을 설정하고 setter로 수정할 수 없게 하면된다.</strong> 즉 setter를 생성하지 않으면 된다.</p>
</li>
<li><p>이렇게 하면 <strong>불변 객체</strong>가 괸다.</p>
</li>
<li><p>또는 setter의 접근 제어자를 private으로 변경.</p>
</li>
<li><p>주의해서 잘 사용하자...</p>
<h2 id="컬렉션-값-타입">컬렉션 값 타입</h2>
</li>
<li><p>값 타입을 컬렉션에 넣어서 사용하는 타입</p>
<ul>
<li>ex) List&lt;Address&gt; addressHistory = ...</li>
</ul>
</li>
<li><p>DB에서는 value만 넣을 수 있지 컬렉션을 담을 구조가 없다.</p>
</li>
<li><p>그래서 보통 별도의 컬렉션 테이블을 생성해야한다.</p>
</li>
<li><p>별도의 컬렉션 테이블은 식별자 PK를 따로 가지지않고 모든 컬럼을 묶어 PK로 구성한다. 당연히 null 입력 X, 중복 저장 X다.
<img src="https://velog.velcdn.com/images/security-won/post/3a3eb602-d3a6-488e-ae94-ea43577e9399/image.png" alt=""></p>
</li>
</ul>
<p>[값 타입 컬렉션 예시]</p>
<pre><code class="language-java">@Entity
public class Member {
    ...

    @ElementCollection
    @CollectionTable(name=&quot;FAVORITE_FOOD&quot;, joinColumns =
    @JoinColumn(name=&quot;member_id&quot;))
    @Column(name=&quot;food_name&quot;)
    private Set&lt;String&gt; favoriteFoods = new HashSet&lt;&gt;();

    @ElementCollection
    @CollectionTable(name=&quot;ADDRESS&quot;, joinColumns =
    @JoinColumn(name=&quot;member_id&quot;))
    private List&lt;Address&gt; addressHistory = new ArrayList&lt;&gt;();

    ...
}</code></pre>
<p>[DB 테이블 생성 결과]</p>
<p> <img src="https://velog.velcdn.com/images/security-won/post/a6adb3d9-1433-4b57-ab16-932eb82141df/image.png" alt="">
직접 작성해보고 생성결과를 봐야 더욱 와닿는 것 같다.
특히 값 타입 수정부분은 나가는 쿼리를 직접 보는 것을 추천한다.</p>
<p>값 타입은 식별자 개념이 없다고 위에서 언급했다.
그래서 값이 변경될 경우 추적이 어려워서 값 타입 컬렉션을 수정할 경우 주인 엔티티와 연관된 모든 데이터를 삭제하고 <strong>현재 값 타입 컬렉션에 있는 값들을 모두 다시 저장</strong>한다........</p>
<p>참고로 값 타입 컬렉션은 영속성 전이와 고가 객체 제거 기능을 필수로 가진다고 볼 수 있다.
그리고 지연 로딩 전략을 사용한다.</p>
<p>값 타입 컬렉션을 사용할 때 매우매우 생각을 많이 해보고 ,,, 꼭 사용해야하는지 고민하자. 일대다 관계로 풀어낼 수 있으면 일대다 관계로 풀어내자</p>
<hr>
<p><a href="https://www.inflearn.com/course/ORM-JPA-Basic">자바 ORM 표준 JPA 프로그래밍-기본편</a>을 학습하면서 정리한 블로그입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 프록시와 연관관계 관리]]></title>
            <link>https://velog.io/@security-won/JPA-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@security-won/JPA-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Wed, 04 Oct 2023 03:40:23 GMT</pubDate>
            <description><![CDATA[<p>회원과 크루 예시를 그대로 가져가겠다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne 
    @JoinColumn(name=&quot;crew_id&quot;)
    private Crew crew;
    ...
}</code></pre>
<pre><code class="language-java">@Entity
public class Crew {
    @Id @GeneratedValue
    private Long id; //crewId

    private String name;    

    @OneToMany(mappedBy=&quot;crew&quot;) // Crew입장에서 Member는 1:N
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
    ...
}</code></pre>
<p>Member를 조회할 때 Crew도 함께 조회해야 할까?</p>
<pre><code class="language-java">Member member = em.find(Member.class, 1L);

printMember(member); // 1. Member만 조회
printMemberAndCrew(member); // 2. Member와 Crew를 함께 조회</code></pre>
<p>만약 Member정보만 출력하고싶고 Crew는 출력하고싶지 않는 경우도 있을 것이다.
사용하지도 않는 정보인 Crew 정보까지 땡겨온다면 뭔가 깔끔하지 않다.
어떤 경우엔 Member와 Crew를 함께 가져오고싶고, 어떤 경우엔 Member만 가져오고싶을 땐 어떻게 해야할까?</p>
<p>JPA는 이러한 문제를 지연 로딩, 프록시 등을 통해 해결한다.</p>
<hr>
<h2 id="프록시">프록시</h2>
<ul>
<li>JPA는 em.find(...) 말고도 em.getReference()라는 메서드도 제공한다.</li>
<li>em.find(...)는 DB를 통해 실제 엔티티 객체를 조회한다.</li>
<li>em.getReference()는 DB 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
즉 DB에 쿼리가 나가지 않는데 객체 조회가 되는 것이다.<pre><code class="language-java">Member member = new Member();
member.setUsername(&quot;hello&quot;);
</code></pre>
</li>
</ul>
<p>em.persist(member);</p>
<p>em.flush();
em.clear();</p>
<p>Member findMember = em.getReference(Member.class, member.getId());</p>
<p>tx.commit();</p>
<pre><code>```text
실행 결과
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (MEMBER_ID, createBy, createdDate, lastModifiedBy, lastModifiedDate, city, street, zipcode, team_TEAM_ID, name) 
        values
            (null, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10월 04, 2023 1:32:44 오전 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/hellojpa;MODE=MYSQL]

Process finished with exit code 0</code></pre><ul>
<li>실행 결과에서 볼 수 있듯이 select쿼리가 나가지 않았다.</li>
<li>하지만 다음과 같이 getReference()를 통해 가져온 값을 직접 사용(getUsername())하면 쿼리문이 나간다.<pre><code class="language-java">...
Member findMember = em.getReference(Member.class, member.getId());
System.out.println(&quot;findMember.getClass() = &quot; + findMember.getClass());
System.out.println(&quot;findMember.getUsername() = &quot; + findMember.getUsername());
</code></pre>
</li>
</ul>
<p>tx.commit();</p>
<pre><code>```text
findMember.getClass() = class hellojpa.Member$HibernateProxy$2gyJwktl
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_4_0_,
        member0_.createBy as createBy2_4_0_,
        member0_.createdDate as createdD3_4_0_,
        member0_.lastModifiedBy as lastModi4_4_0_,
        member0_.lastModifiedDate as lastModi5_4_0_,
        member0_.city as city6_4_0_,
        member0_.street as street7_4_0_,
        member0_.zipcode as zipcode8_4_0_,
        member0_.team_TEAM_ID as team_TE10_4_0_,
        member0_.name as name9_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
findMember.getUsername() = hello</code></pre><ul>
<li>select쿼리문이 나갔다. </li>
<li>findMember().getClass()를 통해 가져온 값을 보면 <del>~</del>Proxy라는 단어가 있는 것을 볼 수 있다. 즉 하이버네이트가 만든 가짜 객체이다. <h3 id="프록시-특징">프록시 특징</h3>
</li>
<li>실제 클래스를 상속받아서 만들어졌다. 그래서 타입 비교시 instance of 사용</li>
<li>그래서 실제 클래스와 겉 모양은 같다.</li>
<li>프록시 객체는 실제 객체의 참조를 보관하고있어 프록시 객체를 호출(ex. findMember.getUsername())하면 프록시 객체는 실제 객체의 메서드를 호출한다.</li>
<li>프록시 객체는 처음 사용할 때 한 번만 초기화한다.</li>
<li>초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는것이 아니라 프록시 객체를 통해 실제 엔티티에 접근이 가능해지는 것</li>
<li>만약 getReference()로 조회할 객체가 영속성 컨텍스트에 존재한다면 실제 엔티티를 반환한다.<ul>
<li>== 비교 시 true값을 반환해야하기 때문에</li>
</ul>
</li>
<li>영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화할 떄 문제가 발생한다.</li>
</ul>
<h4 id="프록시-확인-메서드">프록시 확인 메서드</h4>
<pre><code class="language-java">프록시 인스턴스 초기화 여부
entitiyManagerfactory.getPersistenceUnitUtil.isLoaded(Object entity) 

프록시 클래스 확인 방법
entity.getClass().getName() 

프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity)</code></pre>
<p>약간 헷갈린다면 직접 em.find(...), em.getReference(...)해서 값을 가져온 다음 타입 비교를 해보자.</p>
<hr>
<p>프록시에 대해 알아보았고 프록시에 대한 이해가 있어야 즉시 로딩, 지연 로딩을 깊이 이해할 수 있다.
맨 위에서 언급한 Member를 조회할 때 Crew도 함꼐 조회해야하는지에 대한 질문의 답을 이제 알아보자.</p>
<h2 id="지연-로딩">지연 로딩</h2>
<ul>
<li><p>Member의 정보만 필요한 경우 Crew를 함께 땡겨올 필요가 없으니 JPA는 지연로딩이라는 옵션을 제공한다.</p>
</li>
<li><p>@ManyToOne(fetch = FetchType.LAZY)</p>
<pre><code class="language-java">@Entity
public class Member extends BaseEntity {

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name=&quot;crew_id&quot;)
private Crew crew;
}</code></pre>
</li>
<li><p>FetchType.LAZY로 조회하는 경우 Member클래스만 DB에서 조회하고 Crew는 프록시 객체로 조회한다.</p>
</li>
<li><p>그래서 프록시 객체로 가져온 crew의 값을 직접 사용하는 순간 DB에 쿼리가 나간다.</p>
</li>
<li><p>즉 지연 로딩으로 설정하면 연관된 정보들은 프록시 객체로 가져온다.</p>
</li>
</ul>
<hr>
<h2 id="즉시-로딩">즉시 로딩</h2>
<ul>
<li><p>지연 로딩과 다르게 연관된 것들도 프록시 객체가 아닌 Join해서 DB에서 실제로 조회하도록 하고싶으면 즉시 로딩을 사용하면 된다.</p>
<pre><code class="language-java">@Entity
public class Member extends BaseEntity {

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

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name=&quot;crew_id&quot;)
private Crew crew;
}</code></pre>
</li>
<li><p>즉시로딩보단 지연로딩을 사용하도록 하자.</p>
</li>
<li><p>즉시로딩은 예상치 못한 SQL이 발생하고 N+1문제를 일으키기때문이다.</p>
<ul>
<li>N+1문제에 대해서는 따로 정리할 생각</li>
</ul>
</li>
<li><p>@ManyToOne, @OneToOne은 기본값이 즉시 로딩이어서 값을 변경해줘야함</p>
</li>
<li><p>@OneToMany, @ManyToMany는 기본값이 지연 로딩이다.</p>
</li>
</ul>
<p>직접 즉시 로딩, 지연 로딩을 해보면서 나가는 쿼리문을 눈으로 보는 것을 추천한다.</p>
<hr>
<h2 id="영속성-전이-cascade">영속성 전이 cascade</h2>
<ul>
<li><p>영속성 전이란 특정 엔티티를 영속 상태로 만들 때 <strong>연관된 엔티티도 함께</strong> 영속 상태로 만들고 싶을 때 사용</p>
<h3 id="cascade--casecadetypeall">cascade = CasecadeType.ALL</h3>
</li>
<li><p>CasecadeType.ALL은 연관된 엔티티까지 모두 영속 상태로 만들어준다.</p>
</li>
<li><p>즉 em.persist(parent)할 경우 child까지 다 persist된다.</p>
</li>
<li><p>하지만 연관관계 매핑과는 관련이 없고 단순히 연관된 엔티티도 함께 영속화하는 편리함을 제공</p>
<pre><code class="language-java">@Entity
public class Parent {

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

private String name;

@OneToMany
(mappedBy = &quot;parent&quot;,
cascade = CascadeType.ALL)
private List&lt;Child&gt; childList = new ArrayList&lt;&gt;();
...
}</code></pre>
<pre><code class="language-java">@Entity
public class Child{

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

private String name;

@ManyToOne
@JoinColumn(name = &quot;PARENT_ID&quot;)
private Parent parent;
...
}</code></pre>
</li>
<li><p>아래 코드처럼 em.persist(ch1);을 따로 안해주더라도 영속성 전이 옵션을 통해 ch1,ch2도 영속 상태로 만들 수 있다.</p>
<pre><code class="language-java">Child ch1 = new Child();
Child ch2 = new Child();
</code></pre>
</li>
</ul>
<p>Parent parent = new Parent();
parent.addChild(ch1); // 연관관계 편의 메서드
parent.addChild(ch2); // 연관관계 편의 메서드</p>
<p>em.persist(parent);</p>
<p>tx.commit();</p>
<pre><code>- CasecadeType.ALL뿐만 아니라 PERSIST, REMOVE 등이 있는데 이와 관련한 부분은 우선 넘어가겠다. 필요할 때 찾아쓰도록.

## 고아 객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 고아 객체라고 하는데 이러한 객체를 자동으로 삭제하고 싶을 땐 다음과 같은 설정을 하면된다.
- 주의할 점은 참조하는 곳이 하나일 때 , 특정 엔티티가 개인 소유할 때, @OneToOne, @OneToMany만 가능하다.
```java
@Entity
public class Parent {

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

  private String name;

  @OneToMany
  (mappedBy = &quot;parent&quot;,
  cascade = CascadeType.ALL,
  orphanRemoval = true) //이 부분
  private List&lt;Child&gt; childList = new ArrayList&lt;&gt;();
  ...
}</code></pre><pre><code class="language-java">@Entity
public class Child{

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

  private String name;

  @ManyToOne
  @JoinColumn(name = &quot;PARENT_ID&quot;)
  private Parent parent;
  ...
}</code></pre>
<hr>
<p><a href="https://www.inflearn.com/course/ORM-JPA-Basic">자바 ORM 표준 JPA 프로그래밍-기본편</a>을 학습하면서 정리한 블로그입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 상속관계 매핑]]></title>
            <link>https://velog.io/@security-won/JPA-%EC%83%81%EC%86%8D%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@security-won/JPA-%EC%83%81%EC%86%8D%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Tue, 03 Oct 2023 15:45:30 GMT</pubDate>
            <description><![CDATA[<p>객체는 상속 관계가 있다.
관계형 DB에는 상속 관계가 없다. 하지만 슈퍼타입 서브타입 관계라는 모델링 기법을 통해 상속 관계를 유사하게 표현할 수 있다.
<img src="https://velog.velcdn.com/images/security-won/post/cf95a7d9-9c4d-4f25-b05b-14f719caec6e/image.png" alt="">
이런식으로 구조를 구성해야지 하면 DB는 3가지 방법을 사용해서 구현할 수 있다.</p>
<ol>
<li>각각의 테이블을 생성 = <strong>조인 전략</strong></li>
<li>하나의 통합 테이블로 표현 = <strong>단일 테이블 전략</strong></li>
<li>서브타입 테이블로 변환 (물품으로 묶이는 공통 속성(ex. name, price 등)들을 음반, 영화, 책에 각각 필드로 생성해주는 것, 쉽게 얘기하면 물품 테이블을 없애고 음반, 영화, 책 테이블만 만드는 것) = <strong>구현 클래스마다 테이블 전략</strong></li>
</ol>
<hr>
<pre><code class="language-java">@Entity
public abstract class Item {

  @Id
  @GeneratedValue
  private Long id;

  private String name;
  private int price;
  ...
}</code></pre>
<pre><code class="language-java">@Entity
public class Album extends Item{

  private String artist;
  ...
}</code></pre>
<pre><code class="language-java">@Entity
public class Book extends Item{

  private String author;
  private String isbn;
  ...
}</code></pre>
<pre><code class="language-java">@Entity
public class Movie extends Item{
  private String director;
  private String actor;
  ...
}</code></pre>
<h2 id="조인-전략">조인 전략</h2>
<ul>
<li>조인 전략을 선택할 시 부모 클래스에서 다음과 같이 설정해주어야한다.</li>
<li>@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략</li>
</ul>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략
public abstract class Item {

  @Id
  @GeneratedValue
  private Long id;

  private String name;
  private int price;
  ...
}</code></pre>
<h2 id="단일-테이블-전략">단일 테이블 전략</h2>
<ul>
<li><p>default로 설정된 값이다.</p>
</li>
<li><p>단일 테이블 전략을 선택할 시 부모 클래스에서 다음과 같이 설정해주어야한다.</p>
</li>
<li><p>@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 단일 테이블 전략</p>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 단일 테이블 전략
public abstract class Item {

@Id
@GeneratedValue
private Long id;

private String name;
private int price;
...
}</code></pre>
<h2 id="구현-클래스마다-테이블-전략">구현 클래스마다 테이블 전략</h2>
</li>
<li><p>Item 클래스에 있던 속성들을 다 밑으로 내리는 것</p>
</li>
<li><p>Item 테이블은 생성되지 않고(abstract class로 변경) Item 클래스가 가지고 있던 필드들이 ALBUM, MOVIE, BOOK에 추가됨.</p>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 단일 테이블 전략
public abstract class Item {

@Id
@GeneratedValue
private Long id;

private String name;
private int price;
...
}</code></pre>
</li>
<li><p>하지만 이 전략은 사용하면 안되는 전략이다.....</p>
</li>
</ul>
<hr>
<h2 id="mappedsuperclass">@MappedSuperclass</h2>
<ul>
<li>단순하게 생각해서 여러 클래스에서 사용되는 공통적인 속성이 있을 때 사용하는 어노테이션이다.</li>
<li>객체입장에서 속성만 상속받아 사용하고 싶을 때 사용</li>
<li>상속관계 매핑X, 엔티티X, 테이블과 매핑X</li>
<li>단순 자식 클래스에 매핑 정보만 제공하는 클래스로 직접 생성해서 사용할 일이 없기때문에 추상 클래스를 권장한다.</li>
</ul>
<p>[예시 코드]</p>
<pre><code class="language-java">@MappedSuperclass
public abstract class BaseEntity {

  private String createBy;
  private LocalDateTime createdDate;
  private String lastModifiedBy;
  private LocalDateTime lastModifiedDate;
  ...
}</code></pre>
<pre><code class="language-java">@Entity
public class Member extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = &quot;MEMBER_ID&quot;)
  private Long id;
  ...
}</code></pre>
<hr>
<p><a href="https://www.inflearn.com/course/ORM-JPA-Basic">자바 ORM 표준 JPA 프로그래밍-기본편</a>을 학습하면서 정리한 블로그입니다.</p>
]]></description>
        </item>
    </channel>
</rss>