<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yunha00.log</title>
        <link>https://velog.io/</link>
        <description>풀스택 개발자가 되고 싶은 꿈나무</description>
        <lastBuildDate>Tue, 13 May 2025 13:41:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yunha00.log</title>
            <url>https://velog.velcdn.com/images/yunha_0228/profile/75b869bc-05fb-4aa6-97e9-60123deceb7f/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yunha00.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yunha_0228" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Spring Boot에서 엔티티를 DTO로 변환하는 다양한 방법]]></title>
            <link>https://velog.io/@yunha_0228/Spring-Boot%EC%97%90%EC%84%9C-%EC%97%94%ED%8B%B0%ED%8B%B0%EB%A5%BC-DTO%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@yunha_0228/Spring-Boot%EC%97%90%EC%84%9C-%EC%97%94%ED%8B%B0%ED%8B%B0%EB%A5%BC-DTO%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 13 May 2025 13:41:59 GMT</pubDate>
            <description><![CDATA[<p>Spring Boot로 API를 개발하다 보면, 프론트엔드로 데이터를 보낼 때 보통 엔티티(Entity)를 직접 반환하지 않고, 별도의 DTO(Data Transfer Object)를 만들어서 전달한다. 이번 글에서는 엔티티를 DTO로 변환하는 이유와 다양한 방법, 각각의 방식이 어떤 장단점 및 특징을 가지고 있는지 정리한다.</p>
<hr>
<h2 id="왜-굳이-dto를-만들어서-보내야-할까">왜 굳이 DTO를 만들어서 보내야 할까?</h2>
<p>처음에는 &quot;그냥 엔티티 그대로 반환하면 되지 않을까?&quot;라고 생각할 수 있다. 하지만 실제로는 다음과 같은 이유 때문에 DTO를 만들어 사용하는 것이 좋다.</p>
<h3 id="1-보안-문제-방지">1. <strong>보안 문제 방지</strong></h3>
<p>엔티티에는 사용자 비밀번호, 내부 데이터 등 민감한 정보가 들어 있을 수 있는데, 이 때 엔티티를 그대로 응답으로 내보내면 정보들이 함께 노출될 수 있다.</p>
<h3 id="2-프론트엔드에-맞는-데이터-형태-제공">2. <strong>프론트엔드에 맞는 데이터 형태 제공</strong></h3>
<ul>
<li>프론트에서는 꼭 모든 필드가 필요하지 않다. DTO를 통해 필요한 필드만 추려서 보낼 수 있다.</li>
<li>응답 형식을 프론트와 협의한 대로 정확히 맞춰줄 수 있다.</li>
</ul>
<h3 id="3-유지보수-유리">3. <strong>유지보수 유리</strong></h3>
<p>엔티티 구조가 변경되어도 DTO를 통해 응답 구조를 따로 관리할 수 있다.</p>
<hr>
<h2 id="엔티티를-dto로-바꾸는-3가지-방법">엔티티를 DTO로 바꾸는 3가지 방법</h2>
<p>List 형태의 엔티티를 DTO로 변환하는 과정이다.</p>
<h3 id="1-생성자-사용">1. 생성자 사용</h3>
<pre><code class="language-java">List&lt;AttachmentDTO&gt; attachmentDTOs = attachments.stream()
    .map(a -&gt; new AttachmentDTO(
        a.getName(),
        a.getChangedName(),
        a.getSize(),
        a.getExtension()
    )).toList();</code></pre>
<h3 id="장점">장점</h3>
<ul>
<li>간단하고 빠르게 작성할 수 있다.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>파라미터 순서를 지켜야 하고, 필드가 많아질수록 가독성이 떨어지고 실수할 수 있다.</li>
<li>어떤 값이 어떤 필드에 들어가는지 직관적으로 보이지 않는다.</li>
</ul>
<hr>
<h3 id="2-빌더-패턴-사용">2. 빌더 패턴 사용</h3>
<pre><code class="language-java">@Builder
public class AttachmentDTO {

    private Long attachmentId;
    private String name;
    private String changedName;
    private LocalDateTime createdAt;
    private Long size;
    private String extension;</code></pre>
<p>먼저, 해당 DTO 클래스에 <code>@Builder</code> 어노테이션을 추가해야 한다.
<br /></p>
<pre><code class="language-java">List&lt;AttachmentDTO&gt; attachmentDTOs = attachments.stream()
    .map(a -&gt; AttachmentDTO.builder()
        .name(a.getName())
        .changedName(a.getChangedName())
        .size(a.getSize())
        .extension(a.getExtension())
        .build()
    ).toList();</code></pre>
<h3 id="장점-1">장점</h3>
<ul>
<li>명확하게 어떤 값이 어떤 필드에 들어가는지 확인할 수 있어 가독성이 좋다.</li>
<li>선택적으로 필드를 설정할 수 있어서 유연하다.</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>코드가 약간 길어진다.</li>
</ul>
<hr>
<h3 id="3-정적-팩토리-메소드-사용">3. 정적 팩토리 메소드 사용</h3>
<pre><code class="language-java">public static AttachmentDTO convertDTO(Attachment attachment) {
    return new AttachmentDTO(
        attachment.getName(),
        attachment.getChangedName(),
        attachment.getSize(),
        attachment.getExtension()
    );
}
</code></pre>
<p>DTO 클래스 내부에 정적 메소드로 작성하여 사용한다. 엔티티에서 DTO로 변환하는 메소드라는 뜻으로 다음과 같이 <code>convertDTO</code>라는 이름으로 작성했다.</p>
<pre><code class="language-java">// 사용
List&lt;AttachmentDTO&gt; attachmentDTOs = attachments.stream()
    .map(AttachmentDTO::convertDTO)
    .toList();</code></pre>
<h3 id="장점-2">장점</h3>
<ul>
<li><p><code>AttachmentDTO.convertDTO()</code>처럼 객체를 생성하지 않고 클래스 이름으로 직접 호출할 수 있어서 직관적이다.</p>
</li>
<li><p>변환 로직을 DTO 클래스 내부에 두어 관련 책임을 부여할 수 있다.</p>
</li>
<li><p>명확한 이름을 붙여서 어떤 역할인지 표현할 수 있다.</p>
<ul>
<li>빌더를 사용하는 방식도 <code>@Builder</code>를 통해 정적 메소드를 주입한 것이다. 즉, <code>AttachmentDTO.builder()</code>도 정적 팩토리 메소드처럼 클래스 이름으로 객체를 생성하는 방식이다.  </li>
</ul>
</li>
<li><p>로직이 바뀌더라도 메소드 내부만 수정하면 돼서 유지보수에 좋다.</p>
</li>
</ul>
<h3 id="단점-2">단점</h3>
<ul>
<li>변환 메소드를 작성해야 하므로 약간의 반복 작업이 필요하다.</li>
<li><code>클래스명.메소드()</code> 형태로 전역으로 호출할 수 있기 때문에, 남용하면 설계가 복잡해질 수 있다.</li>
</ul>
<hr>
<h2 id="-onetomany-연관-관계에서의-사용">+ OneToMany 연관 관계에서의 사용</h2>
<p>Post &lt;-&gt; List(Attachment) 연관 관계이고,
Post → PostDetailDTO 변환</p>
<pre><code class="language-java">public static PostDetailDTO convertDTO(Post post, List&lt;AttachmentDTO&gt; attachmentDTOs) {
    return new PostDetailDTO(
        post.getPostId(),
        post.getTitle(),
        post.getContent(),
        post.getCreatedAt(),
        attachmentDTOs
    );
}</code></pre>
<ul>
<li>복합적인 DTO를 만들 때는 여러 값을 함께 받아서 변환하는 메소드를 만들어 사용한다.</li>
</ul>
<hr>
<h2 id="정리">정리</h2>
<p>API 응답을 깔끔하고 안정적으로 만들기 위해서는 DTO 설계와 변환 방식에 신경 써야 한다. 단순히 동작만 되면 끝이 아니라, 나중에 유지보수나 확장할 때 얼마나 편할지를 생각하고 코드를 짜보자.</p>
<p>정적 팩토리 메소드는 처음에는 익숙하지 않을 수 있지만, 점점 큰 프로젝트일수록 의도를 명확히 전달할 수 있고 변환 로직 변경이 필요할 때도 해당 메서드만 수정하면 되어 코드의 가독성과 유지보수성을 높일 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot에서 React로 LocalDateTime 을 받아올 때 타입 문제]]></title>
            <link>https://velog.io/@yunha_0228/Spring-Boot%EC%97%90%EC%84%9C-React%EB%A1%9C-LocalDateTime-%EC%9D%84-%EB%B0%9B%EC%95%84%EC%98%AC-%EB%95%8C-%ED%83%80%EC%9E%85-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@yunha_0228/Spring-Boot%EC%97%90%EC%84%9C-React%EB%A1%9C-LocalDateTime-%EC%9D%84-%EB%B0%9B%EC%95%84%EC%98%AC-%EB%95%8C-%ED%83%80%EC%9E%85-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Tue, 06 May 2025 22:03:58 GMT</pubDate>
            <description><![CDATA[<p>Spring Boot에서 <code>LocalDateTime</code>을 API 응답으로 내려줄 때, 우리가 기대하는 <code>&quot;2025-05-02 03:47:19&quot;</code> 같은 문자열이 아니라 <strong>이상한 숫자 배열</strong>이나 <strong>timestamp 형태</strong>로 응답되는 경우를 많이 겪었을 것이다.</p>
<p>이 글에서는 왜 그런 현상이 생기는지, 그리고 어떻게 해결할 수 있는지 정리했다.</p>
<hr>
<h2 id="왜-날짜가-배열처럼-보일까">왜 날짜가 배열처럼 보일까?</h2>
<p>Spring Boot에서는 기본적으로 <code>Jackson</code> 라이브러리를 통해 객체를 JSON으로 직렬화한다.<br>하지만 <code>LocalDateTime</code>은 Jackson 기본 설정에 의해 이렇게 <code>[year, month, day, hour, minute, second, nano]</code> 배열 형태나 숫자 형태의 <code>timestamp</code>로 처리된다.</p>
<pre><code class="language-json">[2025, 5, 2, 3, 47, 19, 939277000]</code></pre>
<p>혹은</p>
<pre><code>20255234719939277000</code></pre><p>이런 현상은 Spring Boot가 사용하는 JSON 직렬화 도구인 Jackson의 기본 설정 때문이다. Jackson은 Java 8의 <code>LocalDateTime</code>을 자동으로 지원하지 않으며, JSR-310(<code>java.time</code>)을 제대로 처리하기 위해서는 별도 모듈을 등록하거나 설정을 명시적으로 바꿔야 한다.</p>
<h2 id="원인-정리">원인 정리</h2>
<p>Spring Boot는 Jackson을 통해 객체를 JSON으로 직렬화한다. 그러나 Java 8의 <code>LocalDateTime</code>은 Jackson 2.x 기준으로 기본 지원 대상이 아니며, 아래와 같은 문제가 발생한다:</p>
<ul>
<li>Jackson이 JSR-310 타입(<code>LocalDateTime</code>, <code>LocalDate</code>, <code>ZonedDateTime</code>)을 배열 형태로 직렬화</li>
<li><code>ObjectMapper</code>가 <code>WRITE_DATES_AS_TIMESTAMPS</code> 설정을 true로 유지할 경우 timestamp처럼 처리</li>
</ul>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="1-applicationyml-설정-추가">1. application.yml 설정 추가</h3>
<pre><code class="language-yaml">spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    serialization:
      write-dates-as-timestamps: false</code></pre>
<ul>
<li><code>date-format</code>: 우리가 원하는 문자열 형식 지정</li>
<li><code>write-dates-as-timestamps: false</code>: timestamp 형태 사용 금지</li>
</ul>
<p>이 설정을 통해 Jackson은 날짜/시간을 배열이나 timestamp가 아닌, 우리가 원하는 문자열 포맷으로 직렬화한다. 단, 이 방법은 <code>java.util.Date</code>, <code>java.sql.Timestamp</code> 등 구형 날짜 클래스에는 적용되지만, <code>LocalDateTime</code>에는 일부 제한이 있다.</p>
<h3 id="2-필드에-jsonformat-어노테이션으로-명시">2. 필드에 @JsonFormat 어노테이션으로 명시</h3>
<p>엔티티나 DTO 클래스의 필드에 직접 적용하고 싶을 때 어노테이션을 붙이는 것이 확실하다.</p>
<pre><code class="language-java">@JsonFormat(pattern = &quot;yyyy.MM.dd HH:mm:ss&quot;)
private LocalDateTime createdAt;</code></pre>
<p>Jackson이 이 어노테이션을 통해 해당 필드만 원하는 포맷으로 직렬화한다. 포맷을 변경하거나 필드별로 다르게 적용하고 싶은 경우 유용하다.</p>
<hr>
<h2 id="응답-결과-확인">응답 결과 확인</h2>
<p>이제 응답에서 아래처럼 문자열 포맷으로 잘 나오는 걸 확인할 수 있다.</p>
<pre><code class="language-json">&quot;createdAt&quot;: &quot;2025.05.02 03:47:19&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/50ab01ec-bdbb-4a43-a779-58813432f8cb/image.png" alt=""></p>
<hr>
<h2 id="마무리">마무리</h2>
<ul>
<li><code>LocalDateTime</code>은 기본적으로 배열이나 timestamp처럼 내려온다.</li>
<li>프론트엔드와 연동 시 날짜 포맷은 프로젝트 초기에 미리 설정해두면 좋을 것 같다.</li>
<li>전역 설정(<code>application.yml</code>) 또는 개별 설정(<code>@JsonFormat</code>) 중 선택 가능</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot에서 파일 업로드 및 저장 로직]]></title>
            <link>https://velog.io/@yunha_0228/Spring-Boot%EC%97%90%EC%84%9C-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B0%8F-%EC%A0%80%EC%9E%A5-%EB%A1%9C%EC%A7%81-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@yunha_0228/Spring-Boot%EC%97%90%EC%84%9C-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%B0%8F-%EC%A0%80%EC%9E%A5-%EB%A1%9C%EC%A7%81-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 04 May 2025 12:33:56 GMT</pubDate>
            <description><![CDATA[<p>웹 서비스에서 파일 업로드 기능은 매우 자주 사용된다. 이번 글에서는 Spring Boot에서 파일 업로드 시 처리해야 할 주요 작업들과 실제 파일 저장 로직, 그리고 유효성 검증 및 예외 처리에 대해 정리해보려고 한다.</p>
<hr>
<h2 id="왜-파일명을-변경해서-저장해야-할까">왜 파일명을 변경해서 저장해야 할까?</h2>
<p>업로드된 파일을 서버에 저장할 때 원래 파일명을 그대로 저장하면, 같은 이름의 파일이 업로드될 경우 기존 파일이 <strong>덮어 써지는 문제</strong>가 발생한다. 이를 방지하기 위해 <strong>UUID를 활용하여 고유한 파일명</strong>을 생성해 저장해주어야 한다.</p>
<pre><code class="language-java">String changedName = UUID.randomUUID().toString() + &quot;.&quot; + extension;</code></pre>
<hr>
<h2 id="파일-저장-전-처리해야-할-작업들">파일 저장 전 처리해야 할 작업들</h2>
<h3 id="1-파일-확장자-추출하기">1. <strong>파일 확장자 추출하기</strong></h3>
<p>파일 확장자는 파일명에서 마지막 <code>.</code> 이후의 문자열로 얻을 수 있다.</p>
<pre><code class="language-java">String extension = originalName.substring(originalName.lastIndexOf(&quot;.&quot;) + 1);</code></pre>
<blockquote>
<p><code>lastIndexOf(&quot;.&quot;)</code>를 사용하면 파일명 중 마지막 점(<code>.</code>)의 위치를 기준으로 확장자를 정확히 추출할 수 있다.</p>
</blockquote>
<hr>
<h3 id="2-파일-저장-경로-만들기">2. <strong>파일 저장 경로 만들기</strong></h3>
<p>파일은 디스크에 저장할 때 <strong>폴더 + 파일명</strong>으로 경로가 완성되어야 한다.<br>이 경로를 코드에 하드코딩하기보다는 <code>application.yml</code>에 저장하고 <code>@Value</code>로 주입받는 게 유지보수에 유리하다.</p>
<p><strong><code>application.yml</code></strong></p>
<pre><code class="language-yaml">file:
  upload-dir: C:/uploads/myfile/files</code></pre>
<p><strong>코드에서 주입받기</strong></p>
<pre><code class="language-java">@Value(&quot;${file.upload-dir}&quot;)
private String uploadUrl;</code></pre>
<blockquote>
<p>하드코딩(<code>String path = &quot;C:/...&quot;</code>)보다 설정 파일을 사용하는 것이 환경 변경에 유리하고, 코드 수정 없이 배포 가능하다.</p>
</blockquote>
<hr>
<h3 id="3-파일-디렉토리-생성">3. <strong>파일 디렉토리 생성</strong></h3>
<p>업로드 디렉토리가 존재하지 않으면 직접 만들어주어야 한다.</p>
<pre><code class="language-java">File directory = new File(uploadUrl);
if (!directory.exists()) {
    directory.mkdirs();
}</code></pre>
<hr>
<h2 id="실제-파일-저장">실제 파일 저장</h2>
<p>업로드된 파일을 서버 디스크에 저장하려면 <code>transferTo()</code> 메서드를 꼭 호출해줘야 한다. 이걸 하지 않으면 DB에 경로만 저장되고 실제 파일은 존재하지 않는 상황이 생긴다.</p>
<pre><code class="language-java">File saveFile = new File(uploadUrl + File.separator + changedName);
file.transferTo(saveFile); // 서버 디스크에 저장</code></pre>
<p>디렉토리가 존재하지 않는다면 <code>mkdirs()</code>로 먼저 만들어줘야 한다.</p>
<hr>
<h2 id="파일-저장-서비스-전체-로직">파일 저장 서비스 전체 로직</h2>
<pre><code class="language-java">@Transactional
public Object createPostWithFile(PostRequestDTO postRequestDTO, List&lt;MultipartFile&gt; files) {
    Post post = new Post(postRequestDTO.getTitle(), postRequestDTO.getContent());

    if (!files.isEmpty()) {
        File directory = new File(uploadUrl);
        if (!directory.exists()) {
            directory.mkdirs();
        }

        for (MultipartFile file : files) {
            validateFile(file);

            String originalName = file.getOriginalFilename();
            String extension = originalName.substring(originalName.lastIndexOf(&quot;.&quot;) + 1);
            String changedName = UUID.randomUUID().toString() + &quot;.&quot; + extension;
            String path = uploadUrl + File.separator + changedName;

            try {
                file.transferTo(new File(path));
            } catch (IOException e) {
                throw new RuntimeException(&quot;파일 저장 실패&quot;, e);
            }

            Attachment newAttachment = Attachment.builder()
                    .name(originalName)
                    .changedName(changedName)
                    .path(path)
                    .size(file.getSize())
                    .extension(extension)
                    .build();

            post.addAttachment(newAttachment);
        }
    }

    postRepository.save(post);
    return &quot;저장 완료&quot;;
}</code></pre>
<hr>
<h2 id="파일-유효성-검사">파일 유효성 검사</h2>
<p>업로드 가능한 파일에 대해 몇 가지 기준을 두고 필터링해야 한다.
아래 3가지는 기본적으로 검사해주는 게 좋다.</p>
<h3 id="유효성-검사-항목">유효성 검사 항목</h3>
<ul>
<li><strong>파일명 검사</strong>: 비어있거나 null인 경우 거부</li>
<li><strong>파일 크기 제한</strong>: 예: 100MB 이하</li>
<li><strong>확장자 제한</strong>: <code>.jpg</code>, <code>.png</code>, <code>.pdf</code> 등 허용된 확장자만 업로드 가능</li>
</ul>
<pre><code class="language-java">private void validateFile(MultipartFile file) {
    if (file.getOriginalFilename() == null) {
        throw new IllegalArgumentException(&quot;유효하지 않은 파일명입니다.&quot;);
    }

    if (file.getSize() &gt; MAX_FILE_SIZE) {
        throw new IllegalArgumentException(&quot;파일 크기는 100MB를 초과할 수 없습니다.&quot;);
    }

    String extension = file.getOriginalFilename()
                           .substring(file.getOriginalFilename().lastIndexOf(&quot;.&quot;) + 1)
                           .toLowerCase();

    if (!ALLOWED_EXTENSIONS.contains(extension)) {
        throw new IllegalArgumentException(&quot;허용되지 않은 파일 확장자입니다. 허용 확장자: &quot; + ALLOWED_EXTENSIONS);
    }
}</code></pre>
<hr>
<h2 id="예외-처리">예외 처리</h2>
<p>잘못된 요청에 대해서는 <code>IllegalArgumentException</code> 등 구체적인 예외를 던져, 클라이언트가 문제를 쉽게 파악할 수 있도록 한다.
<code>IllegalArgumentException</code>:인자가 잘못되었을 때 발생하는 예외</p>
<hr>
<h2 id="정리">정리</h2>
<p>정리한 파일 업로드 로직은 다음과 같은 요구사항을 충족한다:</p>
<ul>
<li>중복 파일명 문제 방지 (UUID)</li>
<li>유연한 경로 관리 (설정 파일)</li>
<li>안전한 저장 (유효성 검사 및 예외 처리)</li>
<li>실제 디스크 저장 보장</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 연관 관계 설정하기]]></title>
            <link>https://velog.io/@yunha_0228/JPA-%EC%97%B0%EA%B4%80-%EA%B4%80%EA%B3%84-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yunha_0228/JPA-%EC%97%B0%EA%B4%80-%EA%B4%80%EA%B3%84-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 30 Apr 2025 11:38:51 GMT</pubDate>
            <description><![CDATA[<p>파일 업로드 및 다운로드 기능을 구현하면서 게시글(Post)과 첨부파일(Attachment) 간의 연관관계를 설정하고, 파일 업로드까지 구현해봤다.
이번에 내가 배운 내용들을 한 번에 정리해두려고 한다.</p>
<hr>
<h2 id="post와-attachment-연관관계-설정">Post와 Attachment 연관관계 설정</h2>
<h3 id="1-관계-구조">1. 관계 구조</h3>
<ul>
<li>게시글 하나(<code>Post</code>)에 첨부파일 여러 개(<code>Attachment</code>)가 달릴 수 있으니까 <strong>1:N 관계</strong>로 매핑했다.</li>
<li>Post 쪽에서는 다음처럼 설정:</li>
</ul>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;post&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
private List&lt;Attachment&gt; attachments = new ArrayList&lt;&gt;();</code></pre>
<ul>
<li><code>mappedBy = &quot;post&quot;</code>: <code>Attachment</code> 안의 <code>Post post</code> 필드 기준으로 연관관계 매핑</li>
<li><code>cascade = CascadeType.ALL</code>: 게시글 저장 시 첨부파일도 함께 저장</li>
<li><code>orphanRemoval = true</code>: 게시글에서 첨부파일을 제거하면 DB에서도 자동 삭제</li>
<li><code>new ArrayList&lt;&gt;()</code>: 리스트는 NullPointException 방지를 위해 항상 초기화</li>
</ul>
<hr>
<h2 id="연관관계-편의-메서드">연관관계 편의 메서드</h2>
<p>양방향 연관관계일 경우, 양쪽에 모두 값을 설정해줘야 정확한 관계가 만들어진다. 이걸 깜빡하면 매핑이 꼬일 수도 있다. 그래서 아래처럼 편의 메서드를 만들어 사용하는 게 안정적이다.</p>
<pre><code class="language-java">public void addAttachment(Attachment attachment) {
    attachments.add(attachment);
    attachment.setPost(this);
}</code></pre>
<ul>
<li>Post가 첨부파일을 관리하는 게 더 자연스러워 보였다.</li>
<li>실수할 가능성도 줄어든다. 연관관계를 Post 쪽에서 확실히 책임지도록 만들어줌.</li>
</ul>
<hr>
<h2 id="prepersist로-자동-날짜-저장">@PrePersist로 자동 날짜 저장</h2>
<p>매번 <code>createdAt</code> 필드에 현재 시간을 넣는 게 귀찮아서, JPA 라이프사이클 콜백을 이용했다.</p>
<pre><code class="language-java">@PrePersist
public void prePersist() {
    this.createdAt = LocalDateTime.now();
}</code></pre>
<p>이제는 저장 전에 자동으로 날짜가 들어가니까 깔끔하다.</p>
<hr>
<h2 id="파일-업로드-처리-흐름">파일 업로드 처리 흐름</h2>
<h3 id="filelist-개념">FileList 개념</h3>
<p>프론트에서 <code>&lt;input type=&quot;file&quot; multiple&gt;</code>을 사용하면 여러 개 파일을 선택할 수 있는데, 이때 나오는 건 배열이 아니라 <code>FileList</code> 객체다. 배열처럼 생겼지만 진짜 배열은 아니다.</p>
<pre><code class="language-js">e.target.files[0].name           // 파일 이름
e.target.files[0].size           // 파일 크기 (byte 단위)
e.target.files[0].type           // MIME 타입
e.target.files.length            // 파일 개수</code></pre>
<h3 id="formdata로-전송">FormData로 전송</h3>
<p>일반 JSON은 텍스트만 전송할 수 있어서, 파일 전송은 <strong>multipart/form-data</strong> 형식을 사용해야 한다. 이럴 때 <code>FormData</code> 객체를 사용하면 편하다.</p>
<pre><code class="language-js">const formData = new FormData();
formData.append(&quot;title&quot;, newPost.title);
formData.append(&quot;content&quot;, newPost.content);

for (let i = 0; i &lt; selectedFile.length; i++) {
    formData.append(&quot;files&quot;, selectedFile[i]);
}</code></pre>
<p>서버 쪽에서는 <code>@RequestParam</code>이나 <code>List&lt;MultipartFile&gt;</code>로 받을 수 있다.</p>
<pre><code class="language-java">@PostMapping(&quot;/file&quot;)
public ResponseEntity&lt;?&gt; upload(
        @RequestParam String title,
        @RequestParam String content,
        @RequestParam List&lt;MultipartFile&gt; files
) {
    // 저장 로직
}</code></pre>
<h3 id="axios로-전송">Axios로 전송</h3>
<pre><code class="language-js">await axios.post(&#39;http://localhost:8080/file&#39;, formData, {
    headers: {
        &quot;Content-Type&quot;: &quot;multipart/form-data&quot;,
    },
});</code></pre>
<p><code>Content-Type</code>은 생략해도 브라우저가 자동으로 설정해주긴 하지만, 명시적으로 넣는 게 확실하다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번에 연관관계 매핑 + 파일 업로드까지 흐름을 정리하면서 느낀 점:</p>
<ul>
<li>JPA에서 연관관계 설정은 생각보다 실수가 많을 수 있어서, 처음부터 편의 메서드로 안정성 높이는 게 중요하다.</li>
<li>cascade, orphanRemoval 덕분에 저장/삭제가 훨씬 간단해진다.</li>
<li>FormData와 multipart/form-data 구조는 한 번 익혀두면, 이미지 업로드, 파일 첨부 등에서 자주 쓴다.</li>
</ul>
<p>시간 들여 구현하고 나니까 훨씬 구조가 깔끔해진 느낌. 다음엔 파일 다운로드나 미리보기 기능도 붙여볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL Workbench 설치&설정 가이드]]></title>
            <link>https://velog.io/@yunha_0228/MySQL-Workbench-%EC%84%A4%EC%B9%98-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@yunha_0228/MySQL-Workbench-%EC%84%A4%EC%B9%98-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Sat, 28 Oct 2023 21:17:23 GMT</pubDate>
            <description><![CDATA[<p>데이터베이스 툴 중 <strong>MySQL Workbench</strong>를 설치하고 세팅하는 방법을 자세히 적어놓았다. MySQL을 배워두면 Oracle 등 <strong>여러 관계형 데이터베이스를 쉽게 다룰 수 있어서 선택하게 되었다.</strong>
<br /></p>
<h3 id="설치-과정">설치 과정</h3>
<p><a href="https://dev.mysql.com/downloads/windows/installer/8.0.html">MySQL :: Download MySQL Installer</a></p>
<p>MySQL 8 버전을 설치했다. 다운로드를 누르면 가입과 로그인 버튼 2개가 나오는데, 밑에 그냥 다운로드하기를 누르면 <strong>가입 없이 바로 다운로드 받을 수 있다.</strong></p>
<p>설치하고 실행하고나면 다음과 같이 설치 화면이 나온다.
서버와 Workbench를 설치해줘야 하기 때문에 <code>Custom</code>을 선택한다.
<img src="https://velog.velcdn.com/images/yunha_0228/post/1b940204-7019-4429-858e-e46ab141b767/image.png" alt="mysql설치1"></p>
<br />

<h4 id="mysql-servers와-workbench를-화살표⇨를-눌러-오른쪽으로-옮겨준다"><strong>MySQL Servers와 Workbench를 화살표(⇨)를 눌러 오른쪽으로 옮겨준다.</strong></h4>
<p>(Samples and Examples는 예제 코드이므로 상관없음)
<img src="https://velog.velcdn.com/images/yunha_0228/post/ceff34b9-9e2b-4efc-9320-8303a86d4863/image.png" alt="">
<img src="https://velog.velcdn.com/images/yunha_0228/post/ab50dc4f-3859-458a-9e0a-e12efbc3f28a/image.png" alt=""></p>
<br />

<p><strong><code>Execute</code>를 눌러서 설치를 시작한다.</strong>
<img src="https://velog.velcdn.com/images/yunha_0228/post/fd4f9d32-ed22-42db-b2f9-238c92adce2f/image.png" alt=""></p>
<p><strong>next &gt;</strong>
<img src="https://velog.velcdn.com/images/yunha_0228/post/c93f1758-9109-4d68-9b6c-19ad316ec9a9/image.png" alt=""></p>
<br />
<br />


<p><strong>계정 비밀번호 설정</strong></p>
<p>까먹을 수 있으니 간단하게 설정했다.
<img src="https://velog.velcdn.com/images/yunha_0228/post/78220019-6e18-4842-879a-79be1c17e7d1/image.png" alt=""></p>
<p><strong>next &gt;</strong>
<img src="https://velog.velcdn.com/images/yunha_0228/post/2d0a467b-21de-447e-ba71-ad5dc77c6f42/image.png" alt=""></p>
<p><strong>이후 계속 next &gt;</strong>
<img src="https://velog.velcdn.com/images/yunha_0228/post/8a545517-93f1-4f48-8098-3a63d40c96b5/image.png" alt=""></p>
<p><strong>서버와 연결</strong></p>
<p>root 와 비밀번호를 입력해서 위에 성공이 떠야한다.
<img src="https://velog.velcdn.com/images/yunha_0228/post/df3ef6cd-c369-466d-9962-d8529eeea9e5/image.png" alt=""></p>
<br />

<h3 id="경로-설정">경로 설정</h3>
<p>설치하면 <code>C:\Program Files\MySQL\MySQL Server 8.0\bin</code> 위치에 MySQL 실행 파일이 있는 것을 확인할 수 있다.
Windows PowerShell 창을 열어 명령어를 입력한다.  <code>성공: 지정한 값을 저장했습니다.</code> 라고 나오면 컴퓨터에 경로를 잘 설정한 것이다.</p>
<pre><code class="language-powershell">SETX PATH &quot;C:\Program Files\MySQL\MySQL Server 8.0\bin:%PATH%&quot;</code></pre>
 <br />

<h3 id="사용자-계정-등록">사용자 계정 등록</h3>
<p>내부에서만 사용하고 싶으면 주소에 <code>localhost</code>(내 PC), 외부에서도 접속해서 사용 가능하게 하려면 ‘<code>%</code>’를 쓴다.</p>
<pre><code class="language-sql">CREATE USER &#39;계정명&#39;@&#39;주소&#39; identified by &#39;비밀번호&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/75e2cbe6-56ca-4708-a611-d80c67e3bd13/image.png" alt=""></p>
<ul>
<li><strong>SHOW databases</strong> - 현재 존재하는 데이터베이스를 확인할 수 있다.</li>
<li><strong>USE mysql</strong> - 기본으로 제공되는 mysql을 사용할 수 있다.</li>
<li><strong>SELECT * FROM user</strong> - mysql 데이터베이스에서 user를 확인해 계정이 잘 추가된 것을 확인한다</li>
</ul>
<br />
<br />


<h3 id="데이터베이스-생성">데이터베이스 생성</h3>
<pre><code class="language-sql">CREATE DATABASE 이름;</code></pre>
<p>명령어를 실행하고 왼쪽 Schemas 창에 마우스 왼쪽을 클릭해서 <code>refresh all</code>로 새로고침을 해서 데이터베이스가 잘 생성되었는지 확인한다.
<img src="https://velog.velcdn.com/images/yunha_0228/post/8d884ee6-ec57-4941-b9c4-f8f7bd6e7628/image.png" alt=""></p>
<br />

<h3 id="권한-부여">권한 부여</h3>
<p>해당 계정에 데이터베이스에 관한 모든 권한을 부여한다는 명령어이다.</p>
<pre><code class="language-sql">GRANT ALL PRIVILEGES ON 데이터베이스 이름.* TO &#39;계정명&#39;@&#39;%&#39;;</code></pre>
<br />

<h3 id="권한-확인">권한 확인</h3>
<p>그러고 계정이 가지고 있는 권한을 확인할 수 있다.</p>
<pre><code class="language-sql">SHOW GRANTS FOR &#39;계정명&#39;@&#39;%&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/fb0206ec-2afd-4513-b604-d2b566fe3ae3/image.png" alt=""></p>
<br />

<h3 id="계정-연결">계정 연결</h3>
<p>계정을 생성했으면 연결을 해야 원하는 계정에 접속할 수 있다.
MySQL Workbench 홈(집 모양)에 들어가서 저기 + 를 누른다.</p>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/8d483340-dbb2-4061-86da-2ca0f638052c/image.png" alt=""></p>
<br />

<p><strong>설정 정보 입력 후 Test Connection</strong></p>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/d942e6a1-92b7-4282-af3a-ce48ee68a90a/image.png" alt=""></p>
<ul>
<li>*<em>connection name *</em>: 헷갈리지 않게 데이터베이스 이름으로 설정(상관 없음)</li>
<li><strong>username</strong> : 계정 이름</li>
<li><strong>default Schema</strong> : 기본으로 사용할 데이터베이스 이름</li>
</ul>
<br />

<p><strong>계정 비밀번호 입력</strong></p>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/3a2ebea5-9ce0-417a-a5d7-923fffb9a816/image.png" alt=""></p>
<p><strong>연결 완료</strong></p>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/f2aafc04-b289-4129-baa8-223e4cd4e436/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/c4fdebca-21ab-417f-8a2c-74e9919511e2/image.png" alt=""></p>
<p>현재 mtvs 계정에 ‘menudb’라는 데이터베이스(스키마)를 생성했고, menudb에서 mtvs 계정에 모든 권한을 부여한 상태를 만든 것이다.
이제 mtvs 계정에 접속해서 DB를 마음대로 다룰 수 있다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 상속과 오버라이딩]]></title>
            <link>https://velog.io/@yunha_0228/Java-%EC%83%81%EC%86%8D%EA%B3%BC-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9</link>
            <guid>https://velog.io/@yunha_0228/Java-%EC%83%81%EC%86%8D%EA%B3%BC-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9</guid>
            <pubDate>Fri, 13 Oct 2023 10:40:16 GMT</pubDate>
            <description><![CDATA[<h3 id="상속">상속</h3>
<p>상속은 우리가 알고 있는 것처럼 자식이 부모의 것을 그대로 물려받는다는 의미가 자바에서 그대로 적용된다. A 클래스가 B 클래스를 상속받았다고 한다면, 이 때 상속한 B 클래스를 부모 클래스, 상속 받은 A 클래스를 자식 클래스로 역할을 나눌 수 있다.</p>
<p>상속은 <code>extends</code> 키워드를 사용하여 다음과 같이 표현한다.</p>
<pre><code class="language-java">public class A extends B{
 ...
}</code></pre>
<p>그러면 B 클래스는 A 클래스(부모)가 가지고 있는 멤버들을 모두 자기 것처럼 사용할 수 있게 된다. 단, 부모가 제외한 것 빼고 사용할 수 있다. (접근 제한자 private, 생성자)</p>
<p>그리고 자바는 <code>단일 상속</code>만을 지원하고 있어 클래스 하나만 부모클래스로 상속받을 수 있다. 다중 상속이 되지 않는다.</p>
<br />
<br />


<h3 id="상속-관계를-가질-수-있는-조건은">상속 관계를 가질 수 있는 조건은?</h3>
<p><strong>Is-A 관계여야 한다.</strong> ‘~는 ~이다’가 되어야 한다.</p>
<p><code>제품 클래스</code>와 <code>컴퓨터 클래스</code>가 있다고 하자.
<strong>‘컴퓨터는 제품이다’</strong> 맞는 말이며 Is-A 관계이다. 따라서 컴퓨터는 제품 클래스를 상속받을 수 있다. 반대로 <strong>‘제품은 컴퓨터이다’</strong>는 말이 되지 않는다. Is-A 관계가 성립하지 않기 때문에 상속 관계가 될 수 없다.</p>
<br />

<h3 id="오버라이딩overriding">오버라이딩(Overriding)</h3>
<p>상속은 부모의 것을 물려받는다는 개념 보다는 부모 클래스의 확장(extends) 개념을 가진다. 물려받은 것을 자기 것처럼 사용하는 것 뿐만 아니라 자식 클래스의 고유 멤버나 메소드를 작성할 수 있다.</p>
<p>특히 부모가 가진 메소드를 커스터마이징할 수도 있는데, 이를 부모 메소드를 자식 클래스에서 <code>재정의</code>한다고 표현하고 <code>오버라이딩</code> 했다고 한다.</p>
<p>부모가 가지는 메소드 선언부를 그대로 사용하면서, 자식 클래스에서 정의한 메소드대로 동작하도록 구현 몸체 부분을 새롭게 다시 작성, 다시 정의하는 기술이다.(메소드 재정의)</p>
<p>이렇게 메소드 재정의를 하면 메소드를 호출할 시 자식클래스에서 재정의한 메소드가 우선적으로 동작하게 된다.</p>
<pre><code class="language-java">// override 코드</code></pre>
<p>override 코드
<br /></p>
<h3 id="상속을-하는-이유">상속을 하는 이유</h3>
<p><strong>새로운 클래스를 작성할 때 기존에 작성한 클래스를 재사용하기 위함</strong>이다. 코드가 반복적으로 사용될 경우 재사용하면 생산성을 크게 향상시킬 수 있다. 또 공통적으로 사용되는 코드가 부모 클래스에 존재하면 수정 사항이 생길 시 부모 클래스만 수정해도 전체적으로 적용된다.</p>
<p>또한, <strong>다형성을 적용하기 위함</strong>이다. 다형성은 간단히 말하면 하나의 객체가 여러 가지 타입을 가질 수 있게 한 것이다. 상속을 하면 자식 클래스의 타입은 자기 자신과 부모 2가지 타입을 가지게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[깃(Git)과 깃허브(Github) / 깃허브 사용법]]></title>
            <link>https://velog.io/@yunha_0228/git-github</link>
            <guid>https://velog.io/@yunha_0228/git-github</guid>
            <pubDate>Fri, 29 Sep 2023 14:07:41 GMT</pubDate>
            <description><![CDATA[<h3 id="깃git--사용하는-이유">깃(Git) &amp; 사용하는 이유</h3>
<blockquote>
<p>  <strong>깃은 소스 버전 관리 시스템으로, 소스 코드를 수정하고 변경한 내용들을 자동으로 계속 기록되는 것이다.</strong></p>
</blockquote>
<p> 예를 들면, 원래 잘 작동하는 소스 코드가 있는데, 거기에다가 다른 기능을 구현하고 싶어 이리저리 코드를 작성했다고 하자. 그러고 실행해보니 오류가 발생하거나, 원하는 대로 기능이 작동하지 않는다면 어떻게 할까?
 이 때 작성했던 코드를 수정하거나 작성하기 이전의 잘 작성되었던 코드로 돌아가야 하는데, <strong>어떻게 되돌릴 것이냐 복구할 것인가 하는 문제가 발생한다.</strong>
 물론 코드를 작성하기 전 백업을 해두는 방법도 있다. 그러나 많은 코드들을 항상 백업해놓고 관리하기는 어렵기 때문에 Git을 통해 기록들을 계속 저장해두는 것이다.</p>
<p>깃 설치하기 <a href="https://git-scm.com/download/win">Git - Downloading Package</a></p>
<br />

<h3 id="깃허브github--사용하는-이유">깃허브(Github) &amp; 사용하는 이유</h3>
<p> 위와 같이 깃은 소스 이력들을 계속 저장하고 관리하는 거라면,</p>
<blockquote>
<p> <strong>깃허브는 이러한 깃들을 온라인 상에서 자유롭게 쓸 수 있도록 모아놓은 저장소이다.</strong> </p>
</blockquote>
<p>  동시에 여러 사람이 / 같은 버전의 소스를 / 서로 다르게 변경하게 된다면, 서로 버전을 알 수 없는 코드가 생기고 코드가 엉키기 쉽기 때문에 코드를 변경하고 관리하기가 어렵게 된다. 따라서 각 사람들의 깃 버전을 관리해야할 필요가 있고, 그러기 위해서는 서로 코드를 잘 알고 있는 상태가 되어야 한다.
 그래서 깃허브를 통해 여러 사람들과 코드를 공유하고 각각 소스 코드의 버전을 관리하도록 하는 것이다. 여러 사람과 협업해야할 때 필수적으로 사용되고 있다.</p>
<hr>
<br />

<h3 id="github깃허브-사용법">GitHub(깃허브) 사용법</h3>
<p>깃허브에 소스 코드를 올리려면 다음과 같은 과정을 거쳐야 한다. </p>
<pre><code>소스 코드(project) —&gt; staging area —&gt; local repository —&gt; remote repository</code></pre><p>간단히 말하면 프로젝트를 업로드 하기 위해 3가지 과정을 거쳐야 한다는 것이다.</p>
<p>프로젝트 폴더에서 <strong>open git bash here</strong>로 bash 창을 열고 명령어 를 입력을 할 준비를 한다.</p>
<h3 id="1-git-저장소-생성init">1. <strong>Git 저장소 생성(init)</strong></h3>
<p>프로젝트 폴더에 로컬 저장소(.git)를 생성한다.</p>
<pre><code class="language-bash">git init</code></pre>
<p>명령어를 입력하면 다음과 같이 프로젝트 디렉토리에 <strong>.git 저장소</strong>가 생성된다. ‘.git 폴더’가 있는지 확인하고 업로드를 시작한다.</p>
<p><img src="https://velog.velcdn.com/images/yunha_0228/post/db1a2032-5621-481f-8df8-80ab6366104a/image.png" alt=""></p>
<h3 id="2-임시-저장소에-추가add">2. <strong>임시 저장소에 추가(add)</strong></h3>
<pre><code class="language-bash">git add 파일명</code></pre>
<p>업로드하고 싶은 파일을 임시 저장소(staging area)에 추가한다.
<br /></p>
<h3 id="3-로컬-저장소에-저장commit">3. <strong>로컬 저장소에 저장(commit)</strong></h3>
<pre><code class="language-bash">git commit -m &quot;commit 메세지&quot;</code></pre>
<p>임시 저장소에 있던 깃을 로컬 저장소(.git)로 저장한다. 저장할 때 변경 사항과 같은 설명들을 메세지로 남길 수 있다.
<br /></p>
<h3 id="4-로컬-저장소와-원격-저장소깃허브-repository-연결remote">4. <strong>로컬 저장소와 원격 저장소(깃허브 repository) 연결(remote)</strong></h3>
<pre><code class="language-bash">git remote add origin github repository</code></pre>
<p>repository url은 repository에 code 밑에 나와있다.
<img src="https://velog.velcdn.com/images/yunha_0228/post/f81ce306-8ad5-4428-934f-4a7f3afebfdc/image.png" alt=""></p>
<h3 id="5-원격-저장소에-저장-업로드push">5. <strong>원격 저장소에 저장, 업로드(push)</strong></h3>
<pre><code class="language-bash">git push -u origin master(main)</code></pre>
<p>이러면 원격 저장소 깃허브에 코드를 성공적으로 업로드할 수 있다!</p>
<br />
<br />

<h3 id="토큰-발급"><strong>토큰 발급</strong></h3>
<p> 원격 저장소에 업로드를 할 때 토큰을 입력하라고 나온다. 이때 당황하지 않고 토큰을 발급 받으면 된다. 설정에서 맨 아래 Developer Settings 에서 다음과 같이 Generate new token을 통해 발급 받을 수 있다.</p>
<pre><code>settings &gt; developer Settings &gt; Personal access tokens &gt; Tokens(classic) &gt; Generate new token</code></pre><p><img src="https://velog.velcdn.com/images/yunha_0228/post/9e69674f-248d-4258-af26-e5d2a6d43bb3/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>