<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>h0jun_ops.log</title>
        <link>https://velog.io/</link>
        <description>Hi.</description>
        <lastBuildDate>Sat, 22 Jun 2024 13:48:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>h0jun_ops.log</title>
            <url>https://velog.velcdn.com/images/h0jun_ops/profile/c524fb65-3314-4657-9838-58b27dad7bb6/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. h0jun_ops.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/h0jun_ops" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Spring] JWT 토큰 생성과 검증]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-JWT-%ED%86%A0%ED%81%B0-%EC%83%9D%EC%84%B1%EA%B3%BC-%EA%B2%80%EC%A6%9D</link>
            <guid>https://velog.io/@h0jun_ops/Spring-JWT-%ED%86%A0%ED%81%B0-%EC%83%9D%EC%84%B1%EA%B3%BC-%EA%B2%80%EC%A6%9D</guid>
            <pubDate>Sat, 22 Jun 2024 13:48:59 GMT</pubDate>
            <description><![CDATA[<h2 id="jwt-token">JWT Token</h2>
<p>JWT 토큰을 io로 생성해보았으니 직접 코드로 생성해보자</p>
<p>Service 레이어에서 들어갈 정보를 포함하는 토큰을 생성하는 메소드와 이를 검증할 수 있는 메소드를 작성한다.</p>
<pre><code>public String create(
            Map&lt;String, Object&gt; claims,
            LocalDateTime expireAt
    ){
        var key = Keys.hmacShaKeyFor(secretKey.getBytes());

        //LocalDateTime -&gt; Date로 맞춰주는 코드
        var _expireAt = Date.from(expireAt.atZone(ZoneId.systemDefault()).toInstant());

        return Jwts.builder()
                .signWith(key, SignatureAlgorithm.HS256)
                .setClaims(claims)
                .setExpiration(_expireAt)
                .compact();
    }</code></pre><p>claims는 여러 데이터가 들어갈 수 있는 Map으로 이전 포스트에서 작성했던 여러 정보가 포함된다.
expireAt은 언제 만료될지 커스텀할 수 있는 시간이다.</p>
<blockquote>
<p>Keys.hmacShaKeyFor(secretKey.getBytes());</p>
</blockquote>
<p>해당 코드를 통해서 Byte형식의 key를 생성한다.
expiredAt은 현재시간인 LocalDateTime이므로 Date타입으로 바꿔서 _expireAt으로 변경한다.</p>
<p>Jwts토큰을 빌더패턴을 사용해 key, 암호화알고리즘, claims, 만료시간을 지정한다.</p>
<pre><code>public void validation(String token){
        var key = Keys.hmacShaKeyFor(secretKey.getBytes());

        var parser = Jwts.parserBuilder()
                .setSigningKey(key)
                .build();

        try{
            var result = parser.parseClaimsJws(token);
            result.getBody().entrySet().forEach(value-&gt; {
                log.info(&quot;key : {}, value : {}&quot;, value.getKey(), value.getValue());
            });
        } catch (Exception e){
            if(e instanceof SignatureException){
                throw new RuntimeException(&quot;JWT Token Not Valid Exception&quot;);
            } else if (e instanceof ExpiredJwtException) {
                throw new RuntimeException(&quot;JWT Token Expired Exception&quot;);
            } else {
                throw new RuntimeException(&quot;JWT Token Valid Exception&quot;);
            }
        }
    }</code></pre><p>토큰을 검증하는 코드는 다음과 같은데, try - catch문을 참고하면 토큰이 유효한지, 만료됐는지 등의 예외가 발생하는데 해당 예외를 제외한 나머지는 아직 중요하지 않으므로 두개만 잡도록 하자</p>
<p>비밀키를 복호화하고 발행된 토큰을 이용해서 서명된 JWS(Json Web Signature)를 검증해 result에 클레임셋을 반환한다.</p>
<pre><code>var parser = Jwts.parserBuilder()
                .setSigningKey(key)
                .build();
var result = parser.parseClaimsJws(token);</code></pre><p>클레임셋에서 우리가 궁금한 것은 Body부분이기 때문에 getBody로 클레임셋에 존재하는 데이터를 로깅해보자.</p>
<pre><code>result.getBody().entrySet().forEach(value-&gt; {
        log.info(&quot;key : {}, value : {}&quot;, value.getKey(), value.getValue());
        });</code></pre><p>그렇게 되면 토큰과 비밀키가 필요하므로 예외가 발생하는 상황을 만들 수 있다.</p>
<blockquote>
<p>private static String secretKey = &quot;&quot;;</p>
</blockquote>
<p>key에 들어갈 secretKey는 클래스 내에 선언한다. 빈 문자열에 사용자가 정할 Secret Code를 적으면 된다.</p>
<p>Controller코드를 작성하지 않고 테스트 코드로 검증해보자.</p>
<pre><code>@Test
void tokenCreate(){
    var claims = new HashMap&lt;String, Object&gt;();
    claims.put(&quot;user_id&quot;, 999);

    var expired_at = LocalDateTime.now().plusSeconds(20);

    var jwtToken = jwtService.create(claims, expired_at);

    System.out.println(jwtToken);
}</code></pre><p>claims에 &quot;user_id&quot; : 999와 만료시간인 현재로부터 20초 뒤의 시간만 담아서 토큰을 발행해보자.</p>
<pre><code>@Test
void tokenValidation(){

    var token = &quot;토큰 입력칸&quot;;

    jwtService.validation(token);
}</code></pre><p>토큰 입력칸에 발생된 토큰을 넣고 검증하면 된다.
20초 뒤에 토큰이 만료되므로 빠르게 검증해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/h0jun_ops/post/225faadb-00c9-44b7-a0bb-aa5ae2bc0e30/image.png" alt="">
토큰을 발행했고 dot 연산자로 이루어진 것을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/h0jun_ops/post/08655a1e-5a88-41fd-9354-d6dd26b629a9/image.png" alt="">
20초 내에 검증했고 user_id가 999인 것과 시간을 볼 수 있다.
이후 시간이 지나서 검증을 시도했다면
<img src="https://velog.velcdn.com/images/h0jun_ops/post/23f0eabf-fd73-40f7-aaaf-0f94e79e5f82/image.png" alt="">
예외가 발생하는 것을 볼 수 있다.
이는 토큰을 잘못 입력해도 예외를 던진다.</p>
<p>20분의 만료시간을 갖는 토큰을 가지고 직접 디코딩해보자.</p>
<p><img src="https://velog.velcdn.com/images/h0jun_ops/post/312932b1-8d7c-4db0-bf04-c81b4f96ecef/image.png" alt="">
해당 토큰을 <a href="jwt.io">jwt.io</a> encoded 영역에 붙여넣으면
<img src="https://velog.velcdn.com/images/h0jun_ops/post/a7750585-c98b-471e-9d25-58e008e4ad78/image.png" alt="">
시간은 살짝 다르지만 잘 변환이 된 것을 볼 수 있다.
해당 토큰의 비밀키는 fastcampuscourse4chapter01jwtauthentication인데, 4를 3으로 바꾸면 토큰의 SIGNATURE부분도 바뀐다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/ee0b7874-961d-4337-8e63-05be21df2720/image.png" alt="">
다시 비밀키를 설정하고 바뀐 토큰을 넣으면
<img src="https://velog.velcdn.com/images/h0jun_ops/post/1d2a9319-d510-4214-bc5b-6298a44b2496/image.png" alt="">
Signature가 맞지 않다는 메세지로 검증 가능하므로 해당 토큰은 변조된 토큰인 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] JWT 인증]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-JWT-%EC%9D%B8%EC%A6%9D</link>
            <guid>https://velog.io/@h0jun_ops/Spring-JWT-%EC%9D%B8%EC%A6%9D</guid>
            <pubDate>Sat, 22 Jun 2024 12:48:22 GMT</pubDate>
            <description><![CDATA[<h2 id="jwt-json-web-token">JWT (JSON Web Token)</h2>
<p>Json객체를 사용해 정보를 안전하게 전달할 수 있도록 설계된 토큰 기반 인증 방식이다.
Authentication &amp; Authorization을 학습중이며 웹 보안 관련한 중요 표준이 등장했다.</p>
<p>JWT는 URL, HTTP Header, HTTP Form과 같은 방식 등으로 전달할 수 있으며, 서버와 클라이언트 간의 인증 정보를 포함한다.
Authentication은 클라이언트가 누구인지 인증하는 것으로 학습한 바 있다.</p>
<p>JWT는 웹 표준을 따르는 인증방식으로 필요한 정보를 다양하게 가지고 한 객체에 담아 전달할 수 있다.</p>
<p>기본적으로 HTTP는 Stateless를 지향한다.
클라이언트의 상태를 서버가 저장하지 않고 있다는 뜻이다.</p>
<p>이는 서버의 확장성과 트래픽, 의존성에 관해 장점을 가지는 요소로 작용하지만,
Stateful방식과 비교해 같은 데이터가 반복적인 전송으로 네트워크적 성능저하와 데이터노출의 문제가 존재할 수 있다.</p>
<p>이를 보와하기 위해서 토큰 방식의 JWT로 데이터의 압축 및 서명을 할 수 있게 되었다.</p>
<blockquote>
<p>HEADER . PAYLOAD . SIGNATURE</p>
</blockquote>
<p>JWT 토큰의 구조는 위와 같다.</p>
<ul>
<li>HEADER : JWT의 타입, 암호화 알고리즘</li>
<li>PAYLOAD : 클레임 정보 (사용자 ID, 권한, 토큰 발급,만료일 등)</li>
<li>SIGNATURE : HEADER, PAYLOAD가 변조됐는지 검증</li>
</ul>
<p>HEADER와 PAYLOAD는 Base64 형식으로 인코딩한다. 각 토큰의 구조는 JSON형식으로 이루어져 있다.
토큰의 예시를 보자 <a href="https://jwt.io/">JWT.io</a>
<img src="https://velog.velcdn.com/images/h0jun_ops/post/e597ff09-ed5b-4550-968d-854f886ba293/image.png" alt="">
HEADER에 포함된 &quot;alg&quot;에는 HMAC SHA 256방식의 암호화 알고리즘을 지정하고 토큰을 JWT 타입으로 지정했다.</p>
<p>전달하려는 내용을 PAYLOAD에 담았다. 각 name을 클레임이라고 하며 Registered, Public, Private Claim으로 분류한다.</p>
<ul>
<li>Registered : 등록된 클레임으로 iss(issuer), exp(expiration time), sub(subject), aud(audience) 등으로 정의한다.</li>
<li>Public : 공개 클레임으로 자유롭게 정의할 수 있는 클레임인데, 충돌을 방지하기 위해서는 주의해야한다.</li>
<li>Private : 비공개 클레임으로 정보를 공유하기 위해서 만들어진 클레임이다.</li>
</ul>
<p>PAYLOAD에는 암호화되거나 서명되지 않는 데이터가 존재하고, Base64로 인코딩되어 디코딩되는 공개된 데이터이다.
그래서 PAYLOAD에는 중요하거나 결정적인 데이터는 포함하지 않고, 탈취되었을 때 가장 가치가 낮은 데이터를 포함한다.</p>
<p>SIGNATURE는 인코딩된 HEADER와 PAYLOAD, dot문자와 secret코드를 포함한다.
secret 코드는 사용자가 정의할 수 있고, 해시 알고리즘으로 변환한다.</p>
<p>대학교 4학년 정보보호 수업에서 배웠던 암호학이 JWT 토큰을 공부하는 데 사용될줄은 몰랐다.
JWT의 암호화 방식이나 기타 자세한 사항은 시간이 되면 따로 작성해보도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Web] Web Service의 인증]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Web-Web-Service%EC%9D%98-%EC%9D%B8%EC%A6%9D</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Web-Web-Service%EC%9D%98-%EC%9D%B8%EC%A6%9D</guid>
            <pubDate>Sat, 22 Jun 2024 12:14:35 GMT</pubDate>
            <description><![CDATA[<h2 id="authentication">Authentication</h2>
<p>인증은 클라이언트가 &quot;누구&quot;인지 확인하는 과정이다. 기본적으로 ID와 패스워드, 혹은 사용자의 정보를 이용해 인증을 거친다.</p>
<h2 id="authoraization">Authoraization</h2>
<p>인가는 그 &quot;누가&quot; 접근하고자 하는 작업의 리소스 혹은 서비스를 제공해도 되는지를 판단한다.</p>
<p>우리는 대한민국에서 주민등록증을 발급받았다. 만 19세가 지나 주점에서 주류를 구매하기 위해서 주점에 신분증을 제시하고 신원을 확인한다.
주민등록증을 발급받아 우리의 신분을 인증 받았고, 주점에서 신분증을 확인해 주류를 구매하는 권한을 인가받은 것이다.</p>
<p>웹에서는 사용자가 접근하기 위해서 API요청을 하면 서버에서 응답을 내리기 이전 사용자의 인증 정보를 바탕으로 사용자 인증을 진행한다.
등록된 인증정보인지 확인된다면 권한을 응답받아 API권한을 인가받는다.</p>
<h2 id="--http-session">- HTTP Session</h2>
<p>웹 애플리케이션에서 사용자 정보를 저장하는 기술
Request - Response 방식으로 진행되며 HTTP 프로토콜은 Stateless 특성을 가져 상태정보를 유지할 수 없다.
사용자의 세션은 웹 애플리케이션에 접속한 후 일정시간(웹 브라우저 혹은 커넥션이 유지되는)동안 정보가 유지된다.</p>
<ul>
<li>Session 정보는 서버에 저장되고 인증 정보를 검증해 Session ID를 생성해둔다.</li>
<li>세션은 서버에서 관리되며 사용자가 임의로 세션 정보를 조작할 수 없다.</li>
<li>세션 ID는 쿠키를 통해 사용자에게 전달되고 웹 애플리케이션에서 인가한다.</li>
</ul>
<p>서버에 세션이 관리된다는 것은 서버에 저장되는 리소스가 존재한다는 것이다.
그렇다는 것은 보안에 좋지만, 사용자가 많은 웹 브라우저는 부하가 걸릴 수 있다는 뜻이다.</p>
<h2 id="--http-cookie">- HTTP Cookie</h2>
<p>클라이언트의 브라우저에 쿠키를 저장한다.
로그인 정보 혹은 기타 데이터 혹은 설정 등을 쿠키에 저장해 활용할 수 있다.
로그인 과정에서 서버는 쿠키를 이용해 클라이언트의 인증 정보를 유지한다. 유효기간이 존재하고 유효기간이 지나면 쿠키는 삭제된다.</p>
<ul>
<li>서버는 사용자의 로그인을 검증하고 인증을 성공하면 사용자의 고유ID와 쿠키를 생성한다.</li>
<li>서버는 쿠키를 HTTP Header에 포함해 클라이언트에 전송한다.</li>
<li>클라이언트의 로컬 브라우저에 쿠키를 저장한다.</li>
<li>이후 서버에 요청을 보낼 때마다 쿠키를 이용해 HTTP Header에 포함해 전송한다.</li>
<li>서버는 쿠키를 통해 사용자 인증을 진행하고 응답을 내린다.</li>
</ul>
<p>key - value 로 이루어져 있고, 이름, 값, 유효기간, 도메인, 경로 등의 정보를 포함한다.
값을 클라이언트가 가지고 있다는 뜻은 보안에 민감하다는 것이다. 쿠키에 민감한 정보를 포함하는 경우 보안 프로토콜을 통해 암호화해야한다.</p>
<h2 id="--http-header">- HTTP Header</h2>
<p>말 그대로 Header에 정보를 저장한다. HTTP basic auth나, HTTP basic Digest, OAuth 등의 프로토콜을 통해 이루어진다.</p>
<p>그 중 basic같은 경우 <strong>요청 -&gt; 인증요구 -&gt; 인증 -&gt; 성공</strong>의 단계로 진행되고 각 레이어에 해당하는 header가 존재한다.</p>
<p>서버가 인증을 요구함에 따라 클라이언트는 header에 id/pw가 적힌 authorization을 적어 보낸다.
이는 base64라는 인코딩 방식으로 암호화되지만, 복호화가 가능한 평문으로 안전하지 않다.
때문에 Https/TLS와 basic 인증은 함께 사용되어야 안전하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] PointCut Designators]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-PointCut-Designators</link>
            <guid>https://velog.io/@h0jun_ops/Spring-PointCut-Designators</guid>
            <pubDate>Sat, 22 Jun 2024 12:07:37 GMT</pubDate>
            <description><![CDATA[<h2 id="pointcut">@PointCut</h2>
<p>지시자를 통해서 @PointCut 어노테이션의 Aspect를 적용할 위치를 지정할 수 있다.
&quot;PCD&quot;라고 부를 수 있다.</p>
<blockquote>
<table>
<thead>
<tr>
<th>지시자</th>
<th>기준</th>
</tr>
</thead>
<tbody><tr>
<td>execution</td>
<td>리턴타입, 타입, 클래스, 패키지, 메소드, 파라미터 기준</td>
</tr>
<tr>
<td>within</td>
<td>특정 경로에 속하는 모든 메서드</td>
</tr>
<tr>
<td>this, target</td>
<td>Spring bean 객체를 대상으로 함</td>
</tr>
<tr>
<td>args</td>
<td>특정 메서드 파라미터값의 arguments</td>
</tr>
<tr>
<td>bean</td>
<td>인자에 등장한 bean의 모든 메소드</td>
</tr>
</tbody></table>
</blockquote>
<blockquote>
<table>
<thead>
<tr>
<th>Annotation</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>@target</td>
<td>특정 어노테이션이 있는 모든 클래스</td>
</tr>
<tr>
<td>@args</td>
<td>특정 어노테이션이 있는 매개변수를 받는 메소드</td>
</tr>
<tr>
<td>@within</td>
<td>특정 어노테이션이 붙은 모든 타입</td>
</tr>
<tr>
<td>@annotation</td>
<td>특정 어노테이션이 붙은 모든 메소드</td>
</tr>
</tbody></table>
</blockquote>
<p>가장 많이 사용하는 지시자는 execution, within이라고 하는데,
execution을 조금 살펴보자</p>
<p>@PointCut 어노테이션이 지정된 메소드가 적용될 범위를 지정한다.</p>
<p>@PointCut(value = &quot;execution(접근제한자-생략가능|리턴타입|패키지|클래스|메서드|매개변수)&quot;)
각 패턴은 <strong>*(모든포인트)</strong>와 <strong>..(0개 이상)</strong>으로 표현 가능하다.
생략가능한 패턴을 제외하고는 모두 지정해야한다.</p>
<p>지시자 중 가장 자세한 문법으로 매칭될 joinPoint를 지정할 수 있다.
정규식을 이해하고 있다면 작성하기 어렵지 않을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] CRUD abstraction 2]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-CRUD-abstraction-2</link>
            <guid>https://velog.io/@h0jun_ops/Spring-CRUD-abstraction-2</guid>
            <pubDate>Mon, 17 Jun 2024 11:07:22 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅에서 CRUD 기능을 추상화해서 재사용 가능한 코드를 생성했다.</p>
<p>Reply 테이블의 CRUD를 추상화 코드로 변경해보자.</p>
<pre><code>@Service
@RequiredArgsConstructor
public class ReplyService extends CRUDService&lt;ReplyDTO, ReplyEntity&gt; {}</code></pre><pre><code>@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/reply&quot;)
public class ReplyApiController extends CRUDAbstractApiController&lt;ReplyDTO, ReplyEntity&gt; {}</code></pre><p>매우 짧아졌다.
JpaRepository를 상속받는 Repository처럼 각 Reply의 클래스는 상속받는 추상클래스의 구현체를 자동으로 사용하게된다.</p>
<pre><code>@Service
@RequiredArgsConstructor
public class ReplyConverter implements Converter&lt;ReplyDTO,ReplyEntity&gt; {

    private final PostRepository postRepository;

    @Override
    public ReplyDTO toDTO(ReplyEntity replyEntity) {
        return ReplyDTO.builder()
                .id(replyEntity.getId())
                .postId(replyEntity.getPost().getId())
                .userName(replyEntity.getUserName())
                .password(replyEntity.getPassword())
                .status(replyEntity.getStatus())
                .title(replyEntity.getTitle())
                .content(replyEntity.getContent())
                .repliedAt(replyEntity.getRepliedAt())
                .build();
    }

    @Override
    public ReplyEntity toENTITY(ReplyDTO replyDTO) {

        var postEntity = postRepository.findById(replyDTO.getPostId());

        return ReplyEntity.builder()
                .id(replyDTO.getId())
                .post(postEntity.orElseGet(()-&gt;null))
                .userName(replyDTO.getUserName())
                .password(replyDTO.getPassword())
                .status((replyDTO.getStatus() != null) ? replyDTO.getStatus() : &quot;REGISTERED&quot;)
                .title(replyDTO.getTitle())
                .content(replyDTO.getContent())
                .repliedAt(
                        (replyDTO.getRepliedAt() != null) ? replyDTO.getRepliedAt() : LocalDateTime.now())
                .build();
    }
}</code></pre><p>Converter 인터페이스에 존재하는 toENTITY를 구현하기만 하면 된다.</p>
<p>Converter는 과거 포스팅에도 기술했듯이 Controller레이어에 Entity가 보이는 것은 바람직하지 않기 때문에 DTO를 통해 데이터를 전송한다.
Repository레이어에 접근할 때 toENTITY메소드를 이용해 접근하도록 DTO를 Entity로 변환해야한다.</p>
<p>Entity로 변환할 때는 null값을 전송해서는 안된다. DTO와 Entity의 구조가 다를 수 있기 때문에 Entity 객체를 빌드할 때 null값이 내려가지 않도록 주의해야한다.</p>
<p><img src="https://velog.velcdn.com/images/h0jun_ops/post/0466b7e7-8b5d-4ab6-9384-d7f589c45fea/image.png" alt=""></p>
<p>게시글 중 상품 하자의 불만이 있는 12번 id의 게시글에 reply를 달아보자
<img src="https://velog.velcdn.com/images/h0jun_ops/post/ce145e36-ce68-4ba8-bedc-311a9c298375/image.png" alt="">
잘 게시되었으며, 기존에 구현하지 않았던 모든 답변 list를 조회해보자.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/202f9d7a-c1e9-4cc6-a605-4ba8e5e498a0/image.png" alt="">
페이지까지 출력이 되고 reply 리스트가 출력이 된다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/6a4bc7c6-909a-4f27-8208-4a35ca539540/image.png" alt="">
id를 조회해 특정 reply를 열람할 수도 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] CRUD abstraction]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-CRUD-abstraction</link>
            <guid>https://velog.io/@h0jun_ops/Spring-CRUD-abstraction</guid>
            <pubDate>Mon, 17 Jun 2024 10:50:56 GMT</pubDate>
            <description><![CDATA[<h2 id="abstraction">Abstraction</h2>
<p>추상화는 객체지향 프로그래밍의 개념으로 기존 클래스의 공통적 요소, 기능을 추출해서 불필요한 부분을 생략하거나 중요한 부분을 중점으로 개략적으로 구성한 것이다.</p>
<p>프로그래밍에서는 인터페이스와 추상 클래스를 통해서 객체 행동을 정의하는 것으로 설명할 수 있겠다.</p>
<h2 id="abstract-class">Abstract Class</h2>
<p>추상 클래스는 공통 속성 혹은 메소드를 정의하고, 일부 메소드는 하위 클래스에서 구현할 수 있다.</p>
<h2 id="interface">Interface</h2>
<p>인터페이스는 클래스가 구현할 메소드를 선언한다.
실제 구현은 인터페이스를 구현할 클래스에서 모두 구현해야한다.</p>
<p>추상 클래스와 인터페이스는 객체를 생성할 수 없다.
Spring에서는 인스턴스화할 수 없는 클래스는 스프링 컨테이너에 등록할 수 없다.</p>
<p>그렇다면 제목에 나와있는 CRUD는 어떤 방식으로 추상화한다는 것일까</p>
<p>우리는 각 테이블(Board, Post, Reply)의 Controller, db, model, service 레이어의 구현을 진행했었다.</p>
<p>각 테이블은 모두 레이어를 가지고 있고, Controller, Service, Repository의 데이터 교환이 이루어지도록 설계했다.</p>
<p>CRUD의 직접적인 호출은 Controller클래스에서, 
CRUD의 내부 구조는 Service클래스에서,
View와 Repository간 데이터 교환 작업은 Converter로 구현했다.</p>
<p>각 테이블 간 공통적인 기능과 구현부를 crud라는 인터페이스로 묶어보자.</p>
<pre><code>public interface CRUDInterface&lt;DTO&gt; {
    DTO create(DTO t);

    Optional&lt;DTO&gt; read(Long id);

    DTO update(DTO t);

    void delete(Long id);

    API&lt;List&lt;DTO&gt;&gt; list(Pageable pageable);
}</code></pre><p>그리고 Entity와 DTO간 데이터 조작, 교환 또한 인터페이스로 묶었다.</p>
<pre><code>public interface Converter&lt;DTO, ENTITY&gt; {

    DTO toDTO(ENTITY entity);

    ENTITY toENTITY(DTO dto);
}</code></pre><p>Service에서 발생하는 CRUD의 내부 구조는 기본적으로 DTO -&gt; Repository -&gt; Entity, 혹은 그 반대에 있다.</p>
<p>각 테이블의 Entity와 DTO간 CRUD 기능을 추상클래스로 구현할 수 있다.</p>
<pre><code>public abstract class CRUDService&lt;DTO, ENTITY&gt; implements CRUDInterface&lt;DTO&gt; {

    @Autowired(required = false)
    private JpaRepository&lt;ENTITY, Long&gt; jpaRepository;

    @Autowired(required = false)
    private Converter&lt;DTO, ENTITY&gt; converter; //Converter를 상속받은 bean이 있으면 컨테이너에 등록, 없으면 null값

    @Override
    public DTO create(DTO dto) {
        var entity = converter.toENTITY(dto);

        jpaRepository.save(entity);

        var returnDTO = converter.toDTO(entity);

        return returnDTO;
    }

    @Override
    public Optional&lt;DTO&gt; read(Long id) {
        var optionalEntity = jpaRepository.findById(id);

        var dto = optionalEntity.map(
                it -&gt; {
                    return converter.toDTO(it);
                }
        ).orElseGet(() -&gt; null);

        return Optional.ofNullable(dto);
    }

    @Override
    public DTO update(DTO dto) {

        var entity = converter.toENTITY(dto);

        jpaRepository.save(entity);

        var returnDTO = converter.toDTO(entity);

        return returnDTO;
    }

    @Override
    public void delete(Long id) {
        jpaRepository.deleteById(id);
    }

    @Override
    public API&lt;List&lt;DTO&gt;&gt; list(Pageable pageable) {
        var list = jpaRepository.findAll(pageable);

        var pagination = Pagination.builder()
                .page(list.getNumber())
                .size(list.getSize())
                .currentElements(list.getNumberOfElements())
                .totalElements(list.getTotalElements())
                .totalPage(list.getTotalPages())
                .build();

        var dtoList = list.stream()
                .map(it -&gt; {
                    return converter.toDTO(it);
                }).collect(Collectors.toList());

        var response = API.&lt;List&lt;DTO&gt;&gt;builder()
                .body(dtoList)
                .pagination(pagination)
                .build();
        return response;
    }
}</code></pre><p>여기서 등장하는 @AutoWired는 위에서 했던 이야기를 이어받는다.
@Autowired는 주입할 빈이 반드시 존재해야 한다.</p>
<p>(required = false)지시자는 빈에 등록된 클래스를 상속 받는 빈이 있다면 스프링 컨테이너에 등록된 빈을 가져오고, 없다면 null을 반환한다.
때문에 추상클래스인 CRUDService는 @Autowired(required = false) 어노테이션을 통해서 빈이 없어도 예외를 발생시키지 않고 null을 필드에 주입한다.</p>
<p>Controller레이어 또한 추상화할 수 있다.</p>
<pre><code>public abstract class CRUDAbstractApiController&lt;DTO, ENTITY&gt; implements CRUDInterface&lt;DTO&gt;{

    @Autowired(required = false)
    private CRUDService&lt;DTO, ENTITY&gt; crudService;

    @PostMapping(&quot;&quot;)
    @Override
    public DTO create(
            @Valid
            @RequestBody
            DTO t) {
        return crudService.create(t);
    }

    @GetMapping(&quot;/id/{id}&quot;)
    @Override
    public Optional&lt;DTO&gt; read(
            @PathVariable
            Long id) {
        return crudService.read(id);
    }

    @PutMapping(&quot;&quot;)
    @Override
    public DTO update(
            @Valid
            @RequestBody
            DTO t) {
        return crudService.update(t);
    }

    @DeleteMapping(&quot;&quot;)
    @Override
    public void delete(
            @PathVariable
            Long id) {
        crudService.delete(id);
    }


    @GetMapping(&quot;/all&quot;)
    @Override
    public API&lt;List&lt;DTO&gt;&gt; list(
            @PageableDefault
            Pageable pageable) {
        return crudService.list(pageable);
    }
}</code></pre><p>결국 CRUD라는 공통적인 기능을 구현한 하나의 패키지를 재사용해서 테이블마다 필요했던 긴 코드를 작성하지 않게 추상화한 것이다.</p>
<p>포스팅이 길어지므로
다음 포스팅에서 Reply 테이블의 추상화 적용 코드를 보는 것으로 챕터를 마치겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] AOP(Aspect Oriented Programming)]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-AOPAspect-Oriented-Programming</link>
            <guid>https://velog.io/@h0jun_ops/Spring-AOPAspect-Oriented-Programming</guid>
            <pubDate>Sun, 16 Jun 2024 10:55:10 GMT</pubDate>
            <description><![CDATA[<h2 id="aop">AOP</h2>
<p><strong>Aspect Oriented Programming</strong>이란 관점(Aspect)에 따라서 기능을 분리하는 프로그래밍 기법이다.</p>
<p><strong>OOP(Object Oriented Programming)</strong>와 반대되는 개념인 것 같지만, 부족한 면을 보완하기 위해서 만들어졌다.</p>
<p>목적과 기능에 따라 클래스와 객체를 분리해서 만든 OOP에서는 비즈니스와 서비스의 로직을 객체로 만들고 
이를 부품으로 사용하는데 의의를 두고 있지만 어떤 관점에서 어떻게 사용할 것인지, 어떤 식으로 나눠서 사용할 것인지는 정의하지 않는다.</p>
<p>AOP는 애플리케이션에서 가장 중요한 비즈니스 로직이나 전역적인 로직, 혹은 특정 서비스에만 사용되는 로직을 
관점에 따라서 사용하는 정의를 따로 할 수 있게한다.</p>
<p>공통된 로직을 적용한 부분을 횡단 관심사(Cross-Cutting Concerns)라고 하고 이를 모듈화해 하나의 단위로 사용하는 기능을 제공한다. </p>
<ul>
<li><p><strong>컴파일 시점</strong>
컴파일 시점에 적용되는 방식은 AspectJ컴파일러가 .class 파일로 컴파일 하기 전 부가기능을 추가해서 컴파일한다.
이를 aspect와 code를 weaving한다고 한다.</p>
</li>
<li><p><strong>클래스 로딩 시점</strong>
JVM에서 클래스 로더에 .class 파일을 올리는 과정에서 Byte Code를 조작해서 부가기능을 추가한다.</p>
</li>
<li><p><strong>런타임 시점</strong>
애플리케이션이 실행되는 런타임 내에 부가기능을 추가하는 방식이다.
런타임에는 코드 변경이 불가능하므로 프록시를 통해서 부가기능을 추가할 수 있다.
메소드 실행 시점으로 부가기능 추가가 제한된다.</p>
</li>
</ul>
<p>Spring에서는 런타임 시점에 AOP를 적용한다.</p>
<blockquote>
<table>
<thead>
<tr>
<th>Annotation</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>@Aspect</td>
<td>AOP를 정의하는 Class에 할당</td>
</tr>
<tr>
<td>@PointCut</td>
<td>AOP기능을 적용할 지점을 설정</td>
</tr>
<tr>
<td>@Before</td>
<td>JoinPoint 실행 이전</td>
</tr>
<tr>
<td>@After</td>
<td>JoinPoint 실행 이후</td>
</tr>
<tr>
<td>@AfterReturning</td>
<td>메소드 실행 이후 호출 성공 시</td>
</tr>
<tr>
<td>@AfterThrowing</td>
<td>메소드 실행 이후 예외 발생 시</td>
</tr>
<tr>
<td>@Around</td>
<td>메소드 실행 전후</td>
</tr>
</tbody></table>
</blockquote>
<pre><code>@Aspect
@Component
public class TimerAOP {
    @Pointcut(value = &quot;within(com.example.filter.controller.UserApiController)&quot;)
    public void timerPointCut(){}

    @Around(value = &quot;timerPointCut()&quot;)
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println(&quot;메소드 실행 이전&quot;);

        Arrays.stream(joinPoint.getArgs()).forEach(
                it -&gt; {
                    System.out.println(it);
                }
        );

        var stopWatch = new StopWatch();
        stopWatch.start();

        joinPoint.proceed();

        stopWatch.stop();

        System.out.println(&quot;total = &quot; + stopWatch.getTotalTimeMillis());
        System.out.println(&quot;메소드 실행 이후&quot;);
    }
}</code></pre><p>Pointcut으로 어떤 클래스에 적용할 것인지 지정할 수 있다.
해당 클래스는 Spring의 관리 범위 내에 있는 Bean만 적용가능하다.</p>
<p>@Around를 통해서 timerPointCut 메소드 전후를 지정한다.
joinPoint에 존재하는 Argument를 모두 출력하고 해당 메소드가 실행된 시간을 측정해보자
<img src="https://velog.velcdn.com/images/h0jun_ops/post/053cff9f-02c0-417f-951f-d6e84ecfbbc0/image.png" alt="">
Controller에서 메소드를 실행 전 이미 JoinPoint에 들어서서 실행 시간 측정을 시작했다.
joinPoint에 있는 inputStream의 Argument를 출력한 이후 시간을 출력하고 반환했다.</p>
<p>만약 해당 과정 내에서 joinPoint의 전화번호 중 <strong>&#39; - &#39;</strong>을 제거하고 싶을 때, </p>
<pre><code>Arrays.stream(joinPoint.getArgs()).forEach(
                it -&gt; {
                    System.out.println(it);
                    if(it instanceof UserRequest){
                        var tempUser = (UserRequest) it;
                        var phoneNumber = tempUser.getPhoneNumber().replace(&quot;-&quot;, &quot;&quot;);
                        tempUser.setPhoneNumber(phoneNumber);
                    }
                }
        );</code></pre><p>replace 로직을 추가할 수 있고,
<img src="https://velog.velcdn.com/images/h0jun_ops/post/d7349386-8ccd-43c7-a5ef-97d191c9c1e1/image.png" alt=""></p>
<pre><code>var newObj = Arrays.asList(new UserRequest());
joinPoint.proceed(newObj.toArray());</code></pre><p>비어있는 UserRequest를 toArray로 바꿔서 내릴 수 있다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/6376a8e1-9192-4091-b38a-2ed61bf5ff36/image.png" alt="">
응답으로 내려온 정보는 null로 보여지지만 서버에는 정상적으로 넘어온 것을 볼 수 있다.
해당 로직은 암, 복호화를 위해서 빈껍데기를 내리거나 로깅을 위해서 수행할 수 있다.</p>
<pre><code>@Before(value = &quot;timerPointCut()&quot;)
public void before(JoinPoint joinPoint){
    System.out.println(&quot;Before&quot;);
}

@After(value = &quot;timerPointCut()&quot;)
public void after(JoinPoint joinPoint){
    System.out.println(&quot;After&quot;);
}

@AfterReturning(value = &quot;timerPointCut()&quot;, returning = &quot;result&quot;)
public void afterReturning(JoinPoint joinPoint, Object result){
    System.out.println(&quot;After Returning&quot;);
}

@AfterThrowing(value = &quot;timerPointCut()&quot;, throwing = &quot;e&quot;)
public void afterThrowing(JoinPoint joinPoint, Throwable e){
    System.out.println(&quot;After Throwing&quot;);
}</code></pre><p>물론 다른 어노테이션도 확인할 수 있다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/77e915e4-3204-4053-956e-97b520f21cea/image.png" alt="">
Before, After, After Returning 메세지가 찍혀있고, 각 시점마다 차이를 확인할 수 있다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/a1348d75-6012-4019-83af-2f85c45bb754/image.png" alt=""></p>
<p>요청시 Exception을 발생시켜보면 함수 호출 과정에서 예외를 감지했고 
joinPoint.proceed로 메소드를 호출하고나서 예외가 터져 이후 메세지가 출력되지 않았다.</p>
<p>설명하지 않은 지시자는 다음 포스팅에서 진행하도록 하겠다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Interceptor]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Interceptor</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Interceptor</guid>
            <pubDate>Sun, 16 Jun 2024 09:36:02 GMT</pubDate>
            <description><![CDATA[<p>Filter를 이야기할 때 Request를 핸들링하기 전이라는 말을 했다.
interceptor는 핸들링하기 전 요청과 응답을 &quot;낚아챈다&quot;는 의미일 것이다.</p>
<p>Controller에서 핸들링하기 전에 데이터를 가공하거나 참조해서 어떤 작업을 하고자할 때 사용한다.</p>
<p>스프링에서 제공하고 있는 HandlerInterceptor interface를 구현하는 것으로 interceptor를 조작할 수 있다.</p>
<p>Exception Handling처럼 전역적인 작업이 가능하고 다양한 용도를 가지며 특정 handler를 지정할 수 있다.</p>
<h3 id="prehandle">preHandle()</h3>
<p>Controller가 호출되기 전 실행된다.
요청을 가로채 사전 작업이 가능하다.
요청을 계속 전달하거나, 처리를 중단할 수 있다.</p>
<h3 id="posthandle">postHandle()</h3>
<p>Controller의 호출 이후 호출되고 요청이 처리되고나서 후처리 작업을 수행할 수 있다.</p>
<h3 id="aftercompletion">afterCompletion()</h3>
<p>view의 렌더링까지 마친 후 호출된다. 반환한 응답을 로깅하는 등의 작업이 가능하다.</p>
<h2 id="annotation-식별">Annotation 식별</h2>
<p>우리는 OpenApi라는 어노테이션을 만들어서 해당 어노테이션을 달고 있는 요소만 controller에 전달하려고 한다.</p>
<pre><code>@Target(value = {ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenApi {}</code></pre><p>OpenApi는 Method, Type인데, METHOD는 물론 메소드이고
TYPE은 class, interface, enum 등에 적용할 수 있도록 한다.</p>
<pre><code>@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private OpenApiInterceptor openApiInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(openApiInterceptor)
                .addPathPatterns(&quot;/**&quot;); //root 하위 모든 주소를 매핑함
    }
}</code></pre><p>Config 파일을 만들어서 설정을 해야 하는데, 내가 만든 Interceptor를 어느 주소범위까지 적용할지 매핑할 수 있다.
addPathPatterns()를 통해서 어떤 요청단계까지 매핑할 수 있는지 직접 정할 수 있고, 물론 두 주소 이상도 정할 수 있다.</p>
<p>preHanle()을 살펴보자</p>
<pre><code>@Component
public class OpenApiInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // true -&gt; controller 전달, false -&gt; 전달 x
        log.info(&quot;Pre Handle&quot;);

        var handlerMethod = (HandlerMethod) handler;

        var methodLevel = handlerMethod.getMethodAnnotation(OpenApi.class); //Method가 달았나
        if(methodLevel != null){
            log.info(&quot;Method Level&quot;);
            return true;
        }

        var classLevel = handlerMethod.getBeanType().getAnnotation(OpenApi.class); //Class가 달았나
        if(classLevel != null){
            log.info(&quot;Class Level&quot;);
            return true;
        }

        //해당 Annotation을 안 달았음
        log.info(&quot;Not a OpenApi : {}&quot; , request.getRequestURI());
        return false;
    }
}</code></pre><p>@OpenApi라는 어노테이션을 생성했으니 @OpenApi가 붙은 메소드나 클래스는 Cotroller로 전달할 수 있어야 한다.</p>
<p>handler에 담긴 특정 핸들러 데이터를 HandlerMethod로 캐스팅해서 특정 핸들러의 메타데이터를 얻을 수 있다.</p>
<p>메소드에 어노테이션이 달려있는가, Controller 클래스에 어노테이션이 달려있는가를 식별하기 위해서 해당 요청의 handler정보를 이용하면 알 수 있다.
만약 메소드에 어노테이션이 달려있으면 Method Level이라는 로그와 함께 요청이 처리되는 결과가 나올 것이고, 클래스에 달려있다면 Class Level이라는 로그가 찍힐 것이다.</p>
<p>위에서 정한 범위 내 어디에도 @OpenApi 어노테이션이 달려있지 않다면 Not a OpenApi 로그와, 어떤 요청단계에서 매핑이 중단됐는지와 함께 요청이 중단될 것이다.</p>
<pre><code>@RestController
@RequestMapping(&quot;/api/user&quot;)
public class UserApiController {

    @OpenApi
    @PostMapping(&quot;&quot;)
    public UserRequest register(
            @RequestBody
            UserRequest userRequest
    ){
        log.info(&quot;{}&quot;, userRequest);
        return userRequest;
    }

    @GetMapping(&quot;/hello&quot;)
    public void hello(){
        log.info(&quot;hello&quot;);
    }</code></pre><p>register메소드에 @OpenApi가 달려있다. Post요청이 들어오면 정상적으로 응답이 내려올 것이고,
Get요청으로 /hello를 매핑하면 응답이 내려가지 않을 것이다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/b0fb0056-330a-4502-9c77-81da28be2dc3/image.png" alt="">
<img src="https://velog.velcdn.com/images/h0jun_ops/post/7ba6b9f5-f34a-485e-89d6-b33eb46b03c3/image.png" alt="">
Pre Handle 로그이후와  Controller의 filter 이전 Method Level 로그가 찍힌 것을 볼 수 있고, 응답도 정상적으로 내려갔다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/dd19d03f-e1d3-4f6f-a541-86950c2dd142/image.png" alt="">
<img src="https://velog.velcdn.com/images/h0jun_ops/post/60468e1d-3514-4a31-ad12-8012b170db4c/image.png" alt="">
Get요청을 보내보면 Pre Handle 이후 Post Handle과 After Completion 로그도 없으며, 응답도 내려가지 않았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Filter]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Filter</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Filter</guid>
            <pubDate>Sun, 16 Jun 2024 08:17:50 GMT</pubDate>
            <description><![CDATA[<h2 id="filter">Filter</h2>
<p>스프링에서는 Request를 핸들링하기 전 필터링을 거친다.
입력 혹은 출력에 대한 예외나 오류를 확인하는 경우로
클라이언트가 잘못 입력하거나 서버의 로직이 잘못된 경우와 같은 상황의 로그를 확인하기 위함이다.</p>
<pre><code>@PostMapping(&quot;&quot;)
    public UserRequest register(
            @RequestBody
            UserRequest userRequest
    ){
        log.info(&quot;{}&quot;, userRequest);
        return userRequest;
    }</code></pre><p>body를 가져오는 POST 요청이 있다고 해보자.</p>
<pre><code>@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRequest {

    private String name;

    private String phoneNumber;

    private String email;

    private Integer age;
}</code></pre><p>네 가지의 필드 중 몇 가지를 잘못 입력했을 때 우리는 null로 서버에 입력된다는 것을 알고 있다.</p>
<p>그래서 Filter라는 interface를 구현할 수 있다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/dbdc5c53-fd45-49a7-aa14-b3eb87b4b119/image.png" alt="">
이름을 Camel Case로, 전화번호를 잘못 입력했다고 가정해보자.</p>
<pre><code>@Component
public class LoggerFilter implements Filter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

    var req = new HttpServletRequestWrapper((HttpServletRequest) servletRequest);
    var res = new HttpServletResponseWrapper((HttpServletResponse) servletResponse);

    var br = req.getReader();

    var list = br.lines().collect(Collectors.toList());

    list.forEach(it -&gt;{
        log.info(&quot;{}&quot;, it);
    });

    filterChain.doFilter(req, res);
    }
}</code></pre><p>doFilter로 들어오는 요청과 리턴되는 응답에 필터링을 할 수 있다.
요청과 응답을 HttpServlet으로 형변환하고 getReader를 통해 body를 모두 읽어보면 
<img src="https://velog.velcdn.com/images/h0jun_ops/post/c2020a49-4e8f-4e7b-948a-6195d8a36b9c/image.png" alt="">
꽤 성공적으로 요청을 읽어왔지만 </p>
<blockquote>
<p>getReader() has already been called for this request 라는 에러를 발생시켰다.</p>
</blockquote>
<p>이유는 getReader를 통해 body를 받을 때 inputStream을 Filter에서 읽었기 때문에 Controller에서 body를 읽지 못하는 문제가 발생한 것이다.</p>
<pre><code>var req = new ContentCachingRequestWrapper((HttpServletRequest) servletRequest);
var res = new ContentCachingResponseWrapper((HttpServletResponse) servletResponse);
</code></pre><p>Request와 Response를 ContentCachingWrapper 객체로 감싸주면 된다.
이름에 나와있듯이 body를 캐싱해서 가지고 있을 수 있다.</p>
<p>InputStream으로 읽히는 요청을 log에 찍어보기 위해 문자열로 바꿔서 찍는다.</p>
<pre><code>    var reqJson = new String(req.getContentAsByteArray());
    log.info(&quot;req {}&quot;, reqJson);

    var resJson = new String(res.getContentAsByteArray());
    log.info(&quot;res {}&quot;, resJson);</code></pre><p>content를 byte 배열로 읽겠다는 뜻일 것이다.
getReader로 읽지 않고 다른 방식을 취했으니 body에 전달되고 에러도 뜨지 않았을까 ?
<img src="https://velog.velcdn.com/images/h0jun_ops/post/09461016-f393-4886-924a-0786e76a787b/image.png" alt="">
무사히 에러는 뜨지 않았다 !
<img src="https://velog.velcdn.com/images/h0jun_ops/post/45346993-e852-431f-bb7e-93c2aec6960d/image.png" alt="">
하지만 body에는 전달되지 않았다.</p>
<p>getReader와 같은 이유로 어찌됐든 get으로 읽었기 때문에 body에는 전달되지 않은 것이다.
그래서 마지막 response를 body에 덮어씌우는 작업이 필요하다.
response는 내려갔지만 비어있는 body가 전달이된 것이기 때문에 </p>
<pre><code> res.copyBodyToResponse(); </code></pre><p>덮어씌워준다면 
<img src="https://velog.velcdn.com/images/h0jun_ops/post/d8bd5f18-e781-479c-b22e-36a12c22a40a/image.png" alt="">
에러도 없고 response도 잘 내려간 모습을 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Simple Board - 구현 4]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EA%B5%AC%ED%98%84-4</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EA%B5%AC%ED%98%84-4</guid>
            <pubDate>Sun, 16 Jun 2024 07:17:08 GMT</pubDate>
            <description><![CDATA[<h2 id="pagination">Pagination</h2>
<p>사용자가 게시판의 글을 열람하기 위해 게시판을 요청할 때
만약 게시글이 10개가 한 페이지에 존재한다면, 한 번에 다 볼 수 있다.</p>
<p>만약 게시글이 100만개가 한 페이지에 존재한다면, 한 번에 다는 커녕 페이지 로드 속도가 매우 느릴 수도 있다.</p>
<p>매번 요청할 때마다 게시글 전체를 가져오게 되면 매우 불편함을 느낄 것이기 때문에
조금씩 (1~n) 그리고 원할 때 다음 n개를 가져오게 된다.</p>
<p>프론트엔드에서 페이지를 나눠서 보여주기 위해서 몇가지 정보가 필요하다.</p>
<pre><code>public class Pagination {

    private Integer page;

    private Integer size;

    private Integer currentElements;

    private Integer totalPage;

    private Long totalElements;
}</code></pre><p>페이지 번호, 한 페이지의 게시글 크기, 현재 보여줄 페이지 번호, 총 페이지 수, 총 게시글 수가 있다.</p>
<pre><code>@GetMapping(&quot;/all&quot;)
public API&lt;List&lt;PostEntity&gt;&gt; list(
        @PageableDefault(page = 0, size = 10, sort = &quot;id&quot;, direction = Sort.Direction.DESC)
        Pageable pageable
){
    return postService.all(pageable);
}</code></pre><p>모든 게시글 리스트를 보고 싶은데 페이지를 나눌 수 있다.
@PageableDefault로 기본값을 설정하고 id의 내림차순으로 정렬하면 최신글부터 보여줄 수 있겠다.
기본값이 아닐 경우 Query Parameter로 page와 size를 받을 수 있다.</p>
<pre><code>public API&lt;List&lt;PostEntity&gt;&gt; all(Pageable pageable){
        var list = postRepository.findAll(pageable);

        var pagination = Pagination.builder()
                .page(list.getNumber())
                .size(list.getSize())
                .currentElements(list.getNumberOfElements())
                .totalElements(list.getTotalElements())
                .totalPage(list.getTotalPages())
                .build()
                ;

        var response = API.&lt;List&lt;PostEntity&gt;&gt;builder()
                .body(list.toList())
                .pagination(pagination)
                .build();

        return response;
    }</code></pre><p>post 테이블에서 모든 글을 리스트에 담아서 pagination 객체를 builder패턴으로 만들 때
요청으로 받은 page와 size를 담아 객체를 만들어 body에 리스트를 담고 페이지 정보를 붙여서 response를 내린다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/989fe856-08a4-4d5d-8fc5-08ecf2729d80/image.png" alt="">
16개의 글을 작성했고 8개씩 페이지를 만들고 싶다면
<img src="https://velog.velcdn.com/images/h0jun_ops/post/84d792cc-1887-43bd-9ea3-c0dff2a36af8/image.png" alt="">
<img src="https://velog.velcdn.com/images/h0jun_ops/post/0c457be1-68f5-4631-b278-24ff1a071b34/image.png" alt="">
Query Parameter로 전달할 수 있겠다.
우리가 전달할 정보들까지 정확하게 전달하는 것을 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Simple Board - 구현 3]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EA%B5%AC%ED%98%84-3</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EA%B5%AC%ED%98%84-3</guid>
            <pubDate>Sat, 15 Jun 2024 09:45:22 GMT</pubDate>
            <description><![CDATA[<p>Controller에서 Entity를 참조하는 것이 바람직하지 않다는 것을 이전 포스팅에서 말한 바 있다.</p>
<p>그래서 DTO를 통해 Converter로만 데이터를 조작할 수 있도록 만들었다.</p>
<pre><code>public class BoardController {

    private final BoardService boardService;

    @PostMapping(&quot;&quot;)
    public BoardDTO create(
            @Valid
            @RequestBody
            BoardRequest boardRequest
    ){
        return boardService.create(boardRequest);
    }

    @GetMapping(&quot;/id/{id}&quot;)
    public BoardDTO view(
            @PathVariable Long id
    ){
        return boardService.view(id);
    }
}</code></pre><p>Controller에서 BoardEntity를 내리는게 아니라 BoardDTO를 내리는 것이 더 바람직하다.</p>
<pre><code>public class BoardService {

    private final BoardRepository boardRepository;
    private final BoardConverter boardConverter;

    public BoardDTO create(
            BoardRequest boardRequest
    ) {
        var entity = BoardEntity.builder()
                .boardName(boardRequest.getBoardName())
                .status(&quot;REGISTERED&quot;).build();

        var saveEntity = boardRepository.save(entity);
        return boardConverter.toDTO(saveEntity);
    }

    public BoardDTO view(Long id) {
        var entity = boardRepository.findById(id).get();
        return boardConverter.toDTO(entity);
    }
}</code></pre><p>그렇게 되면 Service 레이어에서 BoardDTO를 통해 Entity를 보내는데 이 단계에서 Converter가 DTO를 통제한다.
Repository에서 가져온 Entity를 Converter가 DTO로 변환된 데이터를 Controller부에 전달하는 것이다.</p>
<p>PostEntity또한 같다.</p>
<pre><code>@ManyToOne
@JsonIgnore
@ToString.Exclude
private BoardEntity board;

@OneToMany(
        mappedBy = &quot;post&quot;
)
@Where(clause = &quot;status = &#39;REGISTERED&#39;&quot;)
@Builder.Default
private List&lt;ReplyEntity&gt; replyList = List.of();</code></pre><p>@OneToMany는 Post테이블 또한 reply를 리스트로 관리하기 때문에 ReplyEntity의 필드인 post와 매핑해야한다.
앞서 발생한 loop가 여기서 또 한 번 발생하기 때문에 replyList - 1, post - N 관계를 형성시킨다.</p>
<p>지금까지는 데이터베이스갱신 시 UNREGISTERED상태인 데이터도 응답을 한다.
@Where 어노테이션에서 status가 REGISTERED인 데이터만 내리게 할 수 있다.
물론 BoardEntity의 postList에도 똑같이 적용된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Simple Board - 구현 2]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EA%B5%AC%ED%98%84-2</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EA%B5%AC%ED%98%84-2</guid>
            <pubDate>Sat, 15 Jun 2024 09:19:16 GMT</pubDate>
            <description><![CDATA[<h2 id="recursion-loop">Recursion Loop</h2>
<p>이 전 코드의 Entity 내의 필드를 방문하는 과정에서
BoardEntity 클래스에 게시글 리스트를 id 내림차순으로 정렬되는 postList로 필드에 추가했고, PostEntity타입 리스트로 선언했다.</p>
<pre><code>public class BoardEntity {

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

    private String boardName;

    private String status;


    @Transient
    private List&lt;PostEntity&gt; postList = List.of();
}</code></pre><p>PostEntity 역시 같은 방식으로 답변 리스트를 가지도록 replyList를 필드에 추가했다.</p>
<pre><code>public class PostEntity {

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

    private Long boardId;

    private String userName;

    private String password;

    private String email;

    private String status;

    private String title;

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

    private LocalDateTime postedAt;

    @Transient
    private List&lt;ReplyEntity&gt; replyList = List.of();
}</code></pre><p>이 또한 ReplyEntity타입의 리스트로 선언했다.</p>
<pre><code>@ManyToOne
@JsonIgnore
@ToString.Exclude
private BoardEntity board;</code></pre><p>각 boardEntity의 게시글로 post가 등록되도록 boardId를 BoardEntity 타입으로 변경했고 한 board에 여러 post가 등록될 수 있도록 연관관계를 @ManyToOne 어노테이션을 통해 설정했다.</p>
<pre><code>@ManyToOne
@ToString.Exclude
@JsonIgnore
private PostEntity post;</code></pre><p>reply에서도 postId를 PostEntity 타입으로 변경했다.</p>
<p>이 과정에서 문제가 일어나게 된다.
BoardEntity에 있는 PostEntity가 BoardEntity를 포함하고 있어 다시 BoardEntity를 방문하게 되고, postList를 참조하면 다시 BoardEntity를 방문하는 loop를 생성한다.</p>
<pre><code>@JsonIgnore
@ToString.Exclude</code></pre><p>그래서 존재하는 어노테이션이 JsonIgnore이다.
상위 테이블의 Entity에 포함된 필드가 다시 상위 테이블의 Entity를 필드로 가지고 있을 때 재방문하는 것을 무시(Ignore)하는 어노테이션이다.</p>
<h2 id="dto-converter-layer">DTO, Converter Layer</h2>
<p>Controller에서 각자의 Entity를 반환하는 것은 좋지 않다.
반환하기 위한 데이터를 Entity로 직접 전달하기보다 적절하게 데이터의 조작을 중재하는 것이 좋다.</p>
<p>간단하게 얘기해서 Controller에서 Service레이어에 전달하는 Entity가 직접 전해지면 
Controller에 Service가 의존하게 되는 역방향 의존이 일어나게 된다.</p>
<p>이를 해결하기 위해서 계층간 DTO와 Converter를 이용한다.</p>
<pre><code>@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class BoardDTO {

        private Long id;

        private String boardName;

        private String status;

        private List&lt;PostDTO&gt; postList = List.of();
}</code></pre><pre><code>@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class PostDTO {

    private Long id;

    private Long boardId;

    private String userName;

    private String password;

    private String email;

    private String status;

    private String title;

    private String content;

    private LocalDateTime postedAt;
}</code></pre><p>각 테이블관계마다 DTO를 만들어주고 Converter를 작성한다.</p>
<pre><code>@Service
@RequiredArgsConstructor
public class BoardConverter {

    private final PostConverter postConverter;
    public BoardDTO toDTO(BoardEntity boardEntity){
        var postList = boardEntity.getPostList()
                .stream()
                .map(postConverter::toDTO)
                .collect(Collectors.toList());

        return BoardDTO.builder()
                .id(boardEntity.getId())
                .boardName(boardEntity.getBoardName())
                .status(boardEntity.getStatus())
                .postList(postList)
                .build();
    }
} postList를 반환할 수 있도록 BoardEntity에 존재하는 postList를 참조함</code></pre><pre><code>@Service
public class PostConverter {

    public PostDTO toDTO(PostEntity postEntity){
        return PostDTO.builder()
                .id(postEntity.getId())
                .boardId(postEntity.getBoard().getId())
                .userName(postEntity.getUserName())
                .password(postEntity.getPassword())
                .email(postEntity.getEmail())
                .status(postEntity.getStatus())
                .title(postEntity.getTitle())
                .content(postEntity.getContent())
                .postedAt(LocalDateTime.now())
                .build();
    }
} </code></pre><p>데이터의 변환 과정을 Converter에서만 관리하도록한다.
이는 한 곳에서 데이터를 관리할 수 있는 장점이 있는 반면
Converter에서 오류가 나면 모든 곳에서 오류가 나는 단점도 존재한다.</p>
<p>Controller의 코드는 포스팅이 너무 길어져 다음 글에서 ,,</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Simple Board - 구현]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 15 Jun 2024 05:55:52 GMT</pubDate>
            <description><![CDATA[<h2 id="end-point">End Point</h2>
<p>게시판의 CRUD를 위해 End Point를 개발해보자.
클라이언트가 서버와 통신하고자 하는 URL 경로로 해당 포스트에서는 CRUD 기능을 제공하기 위해 응답 요청을 구현하는 것으로 한다.</p>
<pre><code>@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class BoardRequest {

    @NotBlank
    private String boardName;
}</code></pre><p>board 테이블에 추가할 것은 게시판 이름 뿐이므로 request는 boardName만 작성한다.</p>
<pre><code>@Service
@RequiredArgsConstructor
public class BoardService {

    private final BoardRepository boardRepository;

    public BoardEntity create(
            BoardRequest boardRequest
    ) {
        var entity = BoardEntity.builder()
                .boardName(boardRequest.getBoardName())
                .status(&quot;REGISTERED&quot;).build();

        return boardRepository.save(entity);
    }
}</code></pre><p>service에서 status를 REGISTERED 리터럴로 지정해준다. 그럼 ID는 Auto-Increament를 통해 자동으로 등록된다.</p>
<pre><code>@RestController
@RequestMapping(&quot;/api/board&quot;)
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    @PostMapping(&quot;&quot;)
    public BoardEntity create(
            @Valid
            @RequestBody
            BoardRequest boardRequest
    ){
        return boardService.create(boardRequest);
    }
}</code></pre><p>위와 같은 방법으로 post, reply도 작성할 수 있다.
단, column의 수가 더 많으므로 빠지지 않도록 유의하며 작성한다.</p>
<pre><code>@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class PostRequest {

    @NotBlank
    private String userName;

    @NotBlank
    @Size(min = 4, max = 4)
    private String password;

    @NotBlank
    @Email
    private String email;

    @NotBlank
    private String title;

    @NotBlank
    private String content;
}</code></pre><p>단편적으로 post 테이블의 request 필드는 5개인데, 각각 NotNull을 달고 있으며 필드마다 조건을 달아줄 수 있다ㅏ.</p>
<pre><code>public class PostEntity {

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

    private Long boardId;

    private String userName;

    private String password;

    private String email;

    private String status;

    private String title;

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

    private LocalDateTime postedAt;
}</code></pre><p>post의 엔터티 필드인데, Service에서 엔터티를 만들 때 각 내용이 입력돼야한다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/3cbf028d-289d-425e-94ea-3a03603f7675/image.png" alt="">
QNA 게시판이라는 board를 생성한다.
위에서 boardEntity의 Status가 &quot;REGISTERED&quot;로 입력돼있기 때문에 자동으로 입력된다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/11e2e6b8-f203-465f-812a-c262fab40ba9/image.png" alt="">
POST 요청을 하게되면 200 OK가 떨어진 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/9146078b-7655-486f-8f81-6b3ecfad5107/image.png" alt="">
데이터베이스에도 잘 적용이 됐다.
reply도 마찬가지로 동작한다.</p>
<pre><code>public class ReplyEntity {

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

    private Long postId;

    private String userName;

    private String password;

    private String status;

    private String title;

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

    private LocalDateTime repliedAt;
}</code></pre><p>post entity와 유사한 엔터티를 가지고 있고, </p>
<pre><code>ublic class ReplyService {

    private final ReplyRepository replyRepository;
    private final PostRepository postRepository;

    public ReplyEntity create(
            ReplyRequest replyRequest
    ) {

        var optionalPostEntity = postRepository.findById(replyRequest.getPostId());

        if(optionalPostEntity.isEmpty()){
            throw new RuntimeException(&quot;게시물이 존재하지 않습니다. : &quot; + replyRequest.getPostId());
        }
        var entity = ReplyEntity.builder()
                .post(optionalPostEntity.get())
                .userName(replyRequest.getUserName())
                .password(replyRequest.getPassword())
                .status(&quot;REGISTERED&quot;)
                .title(replyRequest.getTitle())
                .content(replyRequest.getContent())
                .repliedAt(LocalDateTime.now())
                .build();

        return replyRepository.save(entity);
    }

    public List&lt;ReplyEntity&gt; findAllByPostId(Long postId) {
        return replyRepository.findAllByPostIdAndStatusOrderByIdDesc(postId, &quot;REGISTERED&quot;);
    }
}</code></pre><p>같은 내용의 create 메소드이지만, 없을 수 있는 reply에 대한 처리는  optionalPostEntity로 처리한다.
물론 get으로 postId를 가져오는 것은 바람직하지는 않다.
위 내용을 적절하게 개선시킨 내용을 다음 포스팅에서 작성하도록 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Simple Board - 설계]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Simple-Board-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Wed, 12 Jun 2024 13:09:54 GMT</pubDate>
            <description><![CDATA[<h2 id="toy-project">Toy Project</h2>
<p>기본적인 게시판을 개발해보자.</p>
<p>프로젝트의 개발 프로세스가 정해진 규격은 없지만, 무턱대로 서비스 로직을 작성하지는 않을 것이다.</p>
<p>시스템 전체의 설계를 진행하고, 데이터베이스를 설계하는 것이 가장 우선이다.</p>
<p>우리는 비회원 게시판을 개발할 예정이고, 기본 레이아웃은 다음과 같다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/ae3e930a-5ced-431a-8857-400df830d215/image.png" alt="">
<img src="https://velog.velcdn.com/images/h0jun_ops/post/bd924b17-a1d3-47e6-967c-2de08c80cc6d/image.png" alt="">
프론트엔드 단의 디자인은 무시하고 우리는 게시판에 들어갈 데이터를 관리해보자.</p>
<h2 id="system">System</h2>
<ul>
<li>Language : Java 11</li>
<li>Framework : Spring boot 2.7.8</li>
<li>DBMS : MySQL 8.x</li>
<li>DB Library : JPA</li>
</ul>
<h2 id="table-entity">Table Entity</h2>
<p>MySQL WorkBench의 ERD 기능을 통해서 GUI를 이용해 테이블을 그려볼 수 있다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/25edb5c2-1b8f-40c5-abf3-f378e3faf38d/image.png" alt="">
 다음과 같은 테이블과 entity를 가지고 있고, board와 post, post와 reply 테이블은 각각 1:n 관계에 있다.
 <img src="https://velog.velcdn.com/images/h0jun_ops/post/97b159af-2842-48d7-81cb-814e33fe6caf/image.png" alt="">
매우 간단하게 테이블을 만들었다.</p>
<p>각 테이블에 매핑되는 코드를 작성한다.</p>
<pre><code>//board Table
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Entity(name = &quot;board&quot;)
public class BoardEntity {

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

    private String boardName;

    private String status;
}</code></pre><pre><code>//post Table
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Entity(name = &quot;post&quot;)
public class PostEntity {

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

    private Long boardId;

    private String userName;

    private String password;

    private String email;

    private String status;

    private String title;

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

    private LocalDateTime postedAt;
}</code></pre><pre><code>//reply Table
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Entity(name = &quot;reply&quot;)
public class ReplyEntity {

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

    private String userName;

    private String password;

    private String status;

    private String title;

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

    private LocalDateTime repliedAt;
}</code></pre><p>content 필드의 타입은 MySQL에서 TEXT 타입으로 지원하는데,
자바에서 문자열과 매핑하기 위해서 TEXT로 정의해주는 @Column 어노테이션이 필요하다.</p>
<p>다음 포스팅에서 End Point를 개발하는 것으로 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Query Method]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Query-Method</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Query-Method</guid>
            <pubDate>Wed, 12 Jun 2024 11:34:59 GMT</pubDate>
            <description><![CDATA[<h2 id="query">Query</h2>
<p>Quert는 데이터베이스에 특정 정보를 요청하는 것이다.
질의라고도 하며 정보를 검색하기 위한 작성문이다.</p>
<p>쿼리를 작성하기 위해서는 SQL이라는 언어를 학습해야하고, 생각보다 새로 익히기 쉽지 않은 구조로 이루어져있다.</p>
<p>JPA에서는 Query Method를 사용해서 JpaRepository의 상속만으로 쿼리를 작성하는 것처럼 동작시킬 수 있다.</p>
<p>min과 max값 사이의 점수만 출력하도록 하는 쿼리를 작성해보자</p>
<pre><code>SELECT * from user 
where 
score &gt;= min
AND 
score &lt;= max</code></pre><p>보기 쉽도록 의미단위로 잘랐다.</p>
<p>user 테이블에서 모든( * ) 데이터 중
특정 조건(where)을 성립하는 데이터를 검색하도록 작성했다.
그 조건은 min &lt;= score &lt;= max 인 score를 가지는 데이터이다.</p>
<p>하지만 JPA에서는 쿼리를 작성하지 않고 메소드 호출로 같은 동작을 수행하는 것처럼 코드를 작성할 수 있다.</p>
<pre><code>public List&lt;UserEntity&gt; findAllByScoreGreaterThanEqualAndScoreLessThanEqual(int min, int max);</code></pre><p>JpaRepository 를 상속하는 UserRepository에 위와 같은 메소드를 선언한다.</p>
<pre><code>public List&lt;UserEntity&gt; filterScore(int min, int max) {
    return userRepository.findAllByScoreGreaterThanEqualAndScoreLessThanEqual(min, max);
}</code></pre><p>UserService에서 해당 메소드를 파라미터와 함께 호출하는 메소드를 구현하고</p>
<pre><code>@GetMapping(&quot;/min_max&quot;)
public List&lt;UserEntity&gt; filterScore(
        @RequestParam int min,
        @RequestParam int max
){
    return userService.filterScore(min, max);
}</code></pre><p>Controller 부분에서 GET 매핑 요청 메소드를 작성한다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/86fba6df-ace6-43f7-ae8b-9e02a70953fe/image.png" alt="">
현재 user 테이블의 전체 데이터는 다음과 같고, 250 &lt;= score &lt;= 400의 데이터를 추출하도록 해보자
<img src="https://velog.velcdn.com/images/h0jun_ops/post/fa25eb3c-ae1f-4e45-b7cb-3b3d8be731f4/image.png" alt="">
<img src="https://velog.velcdn.com/images/h0jun_ops/post/b5c7e4fb-cb44-4e21-bbae-b0ff444c07d3/image.png" alt="">
성공적으로 응답을 내렸고, 실행로그 하단에 쿼리문이 출력된 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/f2bccca1-4603-4468-bbf8-925448fc050e/image.png" alt="">
파라미터로 전달된 부분을 값으로 입력하고 실제 쿼리를 데이터베이스에 날려보면 같은 데이터를 추출한 것을 볼 수 있다.</p>
<h2 id="query-method">Query Method</h2>
<p>Query Method는 특정 조건으로 쿼리를 파싱한다.</p>
<pre><code>findAllByScoreGreaterThanEqualAndScoreLessThanEqual(int min, int max)</code></pre><p>위 메소드 이름에서find ~ By를 Subject라고 하고 그 뒤의 부분을 Predicate라고 한다.
subject에서 select 쿼리를 작성하는 메서드가 만들어진다.
predicate에서 Camel case로 단어를 쪼개면서 필드를 찾고 의미를 파싱하게 된다.</p>
<p>실제로 위 메소드는 
findAllBy -&gt; predicate 의 데이터를 모두 반환하고</p>
<pre><code>Score/GreaterThanEqual/And/Score/LessThanEqual(int min, int max)</code></pre><p>위 단위로 파싱하는데, 필드로 나타나는 Score를 파라미터 min과 max의 등장 순서대로 매칭하고
나머지 의미단위는 정해진 로직에 의해서 쿼리문에 매칭된다. 아래 링크에서 모든 메소드를 확인할 수 있다.
<a href="https://docs.spring.io/spring-data/jpa/docs/current-SNAPSHOT/reference/html/#jpa.query-methods">Spring Data JPA Reference</a></p>
<pre><code>findAllBy -&gt; select * from user 

Score/Score -&gt; min/max

GreaterThanEqualAndLessThanEqual -&gt; ?? &gt;= min and ?? &lt;= max</code></pre><p>정리하자면 위와 같이 매칭된다고 생각할 수 있겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring Data JPA 2]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Spring-Data-JPA-2</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Spring-Data-JPA-2</guid>
            <pubDate>Tue, 11 Jun 2024 11:58:26 GMT</pubDate>
            <description><![CDATA[<p>Database 파트에서 작성했던 Memory 기반 Database 코드를 JPA 코드로 옮겨보자</p>
<p>Spring Data JPA와 mysql connector dependency를 주입해줘야 한다.</p>
<pre><code>dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    compileOnly &#39;org.projectlombok:lombok&#39;
    runtimeOnly &#39;com.mysql:mysql-connector-j&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testRuntimeOnly &#39;org.junit.platform:junit-platform-launcher&#39;
}</code></pre><p>yaml 파일도 작성해서 데이터베이스와 연동해야한다.</p>
<p>기존 코드에서 레포지터리 관련 클래스는 SimpleDataRepository라는 추상 클래스를 상속한 클래스로 만들었지만,
JPA를 사용하므로 JpaRepository 인터페이스를 상속하는 인터페이스로 변경해준다.</p>
<pre><code>public interface UserRepository extends JpaRepository&lt;UserEntity, Long&gt; {}</code></pre><p>Entity 또한 Primary Key를 등록해줘야한다.</p>
<pre><code>@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name = &quot;user&quot;)
public class UserEntity{

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

    private String name;

    private int score;
}</code></pre><p>동시에 user테이블과 연결해서 데이터베이스와 데이터의 entity를 맞춰준다.
관련해서 book 테이블도 만들어 연결해준다.</p>
<pre><code>@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name = &quot;book&quot;)
public class BookEntity {

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

    private String name;

    private String category;

    private BigDecimal amount;
}</code></pre><p>BookRepository 또한 JpaRepository를 상속하는 인터페이스로 만들어준다.</p>
<pre><code>public interface BookRepository extends JpaRepository&lt;BookEntity, Long&gt; {}</code></pre><p>앞서 살펴봤듯이 SimpleDataRepository에 구현한 내용이 이미 JpaRepository에 선언됐기 때문에 Controller 에서 호출한 메소드는 자동으로 JpaRepository를 상속받게 된다.</p>
<p>데이터 추가와 조회 기능을 모두 JPA로 옮겼고, 
다른 메소드에서 몇가지 오류가 있으므로 다음 작성글에서 수정하도록 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring Data JPA]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-Spring-Data-JPA</link>
            <guid>https://velog.io/@h0jun_ops/Spring-Spring-Data-JPA</guid>
            <pubDate>Tue, 11 Jun 2024 10:35:38 GMT</pubDate>
            <description><![CDATA[<h2 id="jdbc">JDBC</h2>
<p>JDBC(Java DataBase Connectivity)는 Java기반 어플이케이션 데이터베이스 관리를 Java에서 사용할 수 있도록 하는 API이다.
JDBC는 많은 DBMS에 사용 가능하도록 Low Level의 인터페이스로 사용할 DBMS의 드라이버와 DB의 URL, id, passwd가 필요한 매우 날것의 정보가 필요한 API이다.
코드가 길어지면 길어질 수록 쿼리를 작성하거나 코드를 작성할 때 예외가 발생할 수 있는 범위가 늘어나고 매우 Raw한 정보가 코드 내에 등장하는 말그대로 날것의 API이다.</p>
<h2 id="jpa">JPA</h2>
<p>그래서 등장한 것이 JPA(Java Persistence API)이다.
자바클래스에서 객체를 데이터베이스와의 관계를 맵핑하는 방법을 담은 인터페이스를 지원한다.
이를 자바의 ORM을 지원하는 API라고 할 수 있겠다.</p>
<p>ORM(Object-Relational Mapping)은 호환되지 않는 유형의 시스템 간 데이터를 변환하는 프로그래밍 기술로 JPA의 핵심적인 개념이다.
JDBC의 문제점을 기반으로 코드 가독성 증가와 유지보수성, 결합도를 낮추는 역할을 한다.</p>
<h2 id="spring-data-jpa">Spring Data JPA</h2>
<p>JPA의 구현체로 Hibernate, DataNucleus 등의 API를 이용하면 되지만, 여전히 반복적인 문제와 예외처리 간 복잡성이 높다.
이 구현체를 더욱 간단하게 이용하려고 만든 추상 클래스이 Spring Data JPA로 다룰 수 있다.</p>
<p>따라서 JDBC, Hibernate 등을 사용하지 않고 Spring Data JPA를 사용하면 Java 객체로 더욱 쉽게 데이터베이스를 다룰 수 있다.</p>
<pre><code>@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name=&quot;user&quot;)
public class UserEntity {

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

    private String name;

    private Integer age;

    private String email;
}</code></pre><p>DTO를 생성하고 @Entity를 통해서 테이블과 연결한다.
@Id는 변수가 Primary Key라는 뜻이며, @GeneratedValue는 데이터가 생성될 때 IDENTITY 방식으로 생성되는 키라는 뜻이다.</p>
<pre><code>@RestController
@RequestMapping(&quot;/api/user&quot;)
@RequiredArgsConstructor
public class UserApiController {

    private final UserRepository userRepository;

    @GetMapping(&quot;/find-all&quot;)
    public List&lt;UserEntity&gt; findAll(){
        return userRepository.findAll();
    }</code></pre><p>간단하게 테이블을 불러올 수 있는 요청을 구현해보면
UserRepository를 통해서 함수를 호출하는 것을 볼 수 있다.</p>
<pre><code>public interface UserRepository extends JpaRepository&lt;UserEntity, Long&gt; {}</code></pre><p>UserRepository는 JpaRepository를 상속받고 있으므로, JpaRepository의 함수를 사용하는데, 내부를 살펴보면
<img src="https://velog.velcdn.com/images/h0jun_ops/post/eaa4e21f-78a3-4a78-926b-29fa5dfa7bb7/image.png" alt=""></p>
<p>여러가지 기능을 하는 함수를 선언한 것을 볼 수 있다.
우리는 이것들을 불러와서 사용하면 된다.</p>
<pre><code>@GetMapping(&quot;/name&quot;)
    public void autoSave(
            @RequestParam String name
    ){
        var user = UserEntity.builder()
                .name(name)
                .build();
        userRepository.save(user);
    }</code></pre><p>save하는 함수를 작성해보면 아직 name만 구현했기에 builder에 들어갈 필드가 name뿐이므로 이름만 가진 user를 요청하게 된다.
<img src="https://velog.velcdn.com/images/h0jun_ops/post/b6ef710b-affb-44c6-bb31-fc7ee566f61d/image.png" alt="">
RequestParameter로 요청해보면
<img src="https://velog.velcdn.com/images/h0jun_ops/post/30eadc31-a7cf-445f-a992-a7036e7cedf4/image.png" alt="">
user 데이터베이스에 잘 적용이 된 것을 확인할 수 있다.
이제 우리는 코드를 작성한 후 서버를 재실행해도 초기화되지 않고 데이터가 남아있는 데이터베이스를 가지게 된 것이다 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] CREATE, INSERT, DELETE using SQL]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-CREATE-INSERT-DELETE-using-SQL</link>
            <guid>https://velog.io/@h0jun_ops/Spring-CREATE-INSERT-DELETE-using-SQL</guid>
            <pubDate>Tue, 11 Jun 2024 07:44:50 GMT</pubDate>
            <description><![CDATA[<h2 id="create">CREATE</h2>
<p>CREATE는 말그대로 생성이다.
테이블을 생성할 때 사용되는 명령어이다.</p>
<pre><code>CREATE TABLE `테이블 이름` (
`컬럼 이름` Datetype option DEFAULT Default/Expression COMMENT &#39;컬럼 정보&#39;,
...
KEY Option(`키`)
);</code></pre><p>등의 코드로 입력 가능하다.
주의할 것은 테이블과 컬럼, 로우는 작은 따옴표 ( &#39; )가 아니라 ESC 밑에 있는 그것, ( ` )으로 사용한다.
(하지만 DEFAULT나 COMMENT의 문자열은 작은 따옴표를 사용한다..)</p>
<p>컬럼이나 로우, 키를 지정할 때만 백 쿼트( ` )를 사용한다.</p>
<p>여담이지만 작은 따옴표를 Apostrophe, 큰 따옴표를 Quotation mark라고 하고 물결표의 따옴표를 Grave라고 하는데
나는 Single Quote, Double Quote, Back Quote라고 알고 있다,, 뭐가 맞는 걸까</p>
<h2 id="insert">INSERT</h2>
<p>INSERT는 데이터를 생성한다.
생성된 테이블에 데이터를 추가할 때 사용되는 명령어이다.</p>
<pre><code>INSERT INTO `테이블 이름`
(
    `컬럼1 이름`,
    `컬럼2 이름`,
    ...
)
VALUES
(
    컬럼1 데이터값(int),
    &#39;컬럼2 데이터값&#39;(varchar),
    ...
)</code></pre><p>데이터의 속성에 따라서 입력하는데, 만약 컬럼이 NOT NULL일 경우 적지 않으면 데이터가 생성되지 않는다.
반대로 NOT NULL이 아닌 key는 비우고 등록할 수 있는데, 정해놓은 DEFAULT값으로 지정된다.</p>
<h2 id="update">UPDATE</h2>
<p>테이블의 내용을 변경할 때 사용한다.</p>
<pre><code>UPDATE `테이블 이름` SET
[key] = [value],
[key] = [&#39;value&#39;],
...
WHERE
[조건절]
and
[조건절]
...
</code></pre><p>내가 원하는 데이터를 찾기 위해서 바꾸기를 원하는 값과 조건절을 잘 이용해야한다.</p>
<h2 id="delete">DELETE</h2>
<p>데이터를 삭제한다.</p>
<pre><code>DELETE FROM `테이블 이름`
WHERE
[조건절],
...</code></pre><p>변경과 같은 원리로 삭제하기 원하는 값의 조건을 잘 입력해야한다.</p>
<h2 id="truncate">TRUNCATE</h2>
<p>TRUNCATE는 테이블의 컬럼 속성은 유지한채로 테이블을 모두 비우는 clean기능을 가진다.</p>
<pre><code>TRUNCATE `테이블 이름`</code></pre><h2 id="drop">DROP</h2>
<p>DROP은 테이블 자체를 삭제하는 기능을 한다. 테이블의 데이터를 모두 없앤다.</p>
<pre><code>DROP `테이블 이름`</code></pre><p>위 내용 모두는 workbench에서 GUI를 통해 마우스 클릭으로 쉽게 이루어질 수 있다.</p>
<p>쿼리문을 작성하기 힘들거나 모르겠다면 이를 이용해도 된다.
테이블 생성을 예로 들어보자
<img src="https://velog.velcdn.com/images/h0jun_ops/post/dd816960-de37-446e-8887-6d0d74b75aa9/image.png" alt="">
테이블 이름과 문자열의 인코딩 방식(생략가능)을 지정한다.
컬럼의 이름과 데이터 타입을 지정해주고 해당 컬럼의 옵션을 체크해주면 </p>
<pre><code>`컬럼 이름` Datetype option DEFAULT Default COMMENT Expression, 와 같은 기능을 한다.</code></pre><p>id에 PK, NN, AI은 각각 PRIMARY KEY, NOT NULL, AUTO_INCREAMENT역할을 한다.
Default/Expression에 값이나 식을 입력하면 데이터를 생성할 때 값을 넣지 않을 경우 자동으로 입력된다.</p>
<p>하단의 COMMENT란에 컬럼 정보를 입력하면 각 컬럼마다 정보를 따로 입력할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] SQL Query using mysql]]></title>
            <link>https://velog.io/@h0jun_ops/Spring-SQL-Query-using-mysql</link>
            <guid>https://velog.io/@h0jun_ops/Spring-SQL-Query-using-mysql</guid>
            <pubDate>Mon, 10 Jun 2024 14:47:35 GMT</pubDate>
            <description><![CDATA[<h2 id="sql">SQL</h2>
<p>SQL은 &#39;Structured Query Language&#39;로 데이터베이스에서 데이터를 다루는데 사용되는 언어이다.</p>
<p>대부분의 데이터베이스 시스템에서 SQL 언어를 지원한다.
오라클에서 제공하는 OracleDB, MySQL이나 MS에서 제공하는 MSSQL 등 여러 DBMS에따라 차이가 존재하긴 하다.
우리는 오픈소스로 제공하는 MySQL의 무료버전을 이용해 쿼리를 간단하게 배운다.</p>
<p>Docker 컨테이너 위에 실행되는 서버에서 MySQL을 위한 GUI 가이드 시스템인 MySQL Workbench를 이용하기로 한다.
WSL이나 VMWare 등 가상시스템을 사용해본 사람이라면 Windows 사용자 중 WSL, hyperV 기능이 꺼져있는 경우가 있을 수 있는데, 
도커를 실행할 때 &#39;docker desktop - unexpected wsl error&#39;를 마주할 수 있다.</p>
<p>위 기능을 키고 각자 부팅 시 바이오스 설정에서 가상설정을 활성화해줘야 한다.
내 PC는 AMD사의 CPU 사용으로 SVM Mode를 활성화했다.</p>
<p>CRUD의 기능을 가지는 명령어 집합은 DDL, DML, DCL, TCL로 나뉜다.</p>
<h2 id="ddl">DDL</h2>
<p>Data Definition Language
데이터를 정의하는데 사용되는 명령어이다.</p>
<blockquote>
<table>
<thead>
<tr>
<th>명령어</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>CREATE</td>
<td>테이블 생성</td>
</tr>
<tr>
<td>ALTER</td>
<td>테이블 구조 변경</td>
</tr>
<tr>
<td>DROP</td>
<td>테이블 삭제</td>
</tr>
<tr>
<td>RENAME</td>
<td>테이블 이름 변경</td>
</tr>
<tr>
<td>COMMENT</td>
<td>테이블 및 컬럼 주석 추가</td>
</tr>
<tr>
<td>TRUNCATE</td>
<td>데이터 초기화</td>
</tr>
</tbody></table>
</blockquote>
<h2 id="dml">DML</h2>
<p>Data Manipulation Language
데이터를 조작하는데 사용되는 명령어.</p>
<blockquote>
<table>
<thead>
<tr>
<th>명령어</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>SELECT</td>
<td>데이터를 조회</td>
</tr>
<tr>
<td>INSERT</td>
<td>데이터 삽입</td>
</tr>
<tr>
<td>UPDATE</td>
<td>데이터 업데이트</td>
</tr>
<tr>
<td>DELETE</td>
<td>데이터 삭제</td>
</tr>
</tbody></table>
</blockquote>
<h2 id="dcl">DCL</h2>
<p>Data Control Language
데이터를 제어하는데 사용되는 명령어.</p>
<blockquote>
<table>
<thead>
<tr>
<th>명령어</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>GRANT</td>
<td>특정 사용자에게 권한 부여</td>
</tr>
<tr>
<td>REVOKE</td>
<td>특정 사용자의 권한 회수</td>
</tr>
<tr>
<td>COMMIT</td>
<td>트랜잭션의 작업이 정상적으로 완료</td>
</tr>
<tr>
<td>ROLLBACK</td>
<td>트랜잭션 작업이 비정상적으로 종료, 원복</td>
</tr>
</tbody></table>
</blockquote>
<p>DCL에서 COMMIT 명령어와 ROLLBACK (+SAVEPOINT) 명령어는 TCL(Transaction Control Language)로 분류하는 경우도 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] @RequiredArgsConstructor in Lombok]]></title>
            <link>https://velog.io/@h0jun_ops/Database-and-WEB-2.1</link>
            <guid>https://velog.io/@h0jun_ops/Database-and-WEB-2.1</guid>
            <pubDate>Sun, 09 Jun 2024 10:05:28 GMT</pubDate>
            <description><![CDATA[<h2 id="requiredargsconstructor">@RequiredArgsConstructor</h2>
<p>스프링 컨테이너는 객체를 생성하고 관리한다. 애플리케이션에서 불필요하게 길어지는 코드를 줄여주고
간단하게 의존성을 주입해주는 역할을 한다.</p>
<p>Lombok에서 제공하는 어노테이션으로 final로 선언된 필드나 NonNull이 붙은 필드의 생성자를 생성해준다.</p>
<p>전통적인 방법으로 @AutoWired가 존재한다.</p>
<pre><code>@Configuration
public class DataBaseConfig {

    // Bean으로 만들어져 Spring의 관리를 받는 객체가 됨
    @Bean
    public UserRepository userRepository(){
        return new UserRepository();
    }
}</code></pre><p>DB의 설정이라고 가시적으로 확인 가능한 @Configuration이라는 어노테이션을 명시해주면,
해당 클래스의 @Bean 어노테이션을 달고 있는 메소드를 스프링 컨테이너에서 빈으로 관리한다.</p>
<pre><code>@Service
public class UserService {

    @AutoWired
    public final UserRepository userRepository;
    ...</code></pre><p>그렇게 @Service를 명시해놓은 클래스 에서 객체를 만들지 않고 필드를 통해서 객체를 싱글톤으로 관리할 수 있다.</p>
<p>바로 config 파일을 따로 관리하는 불편함을 없애주는 것이
@RequiredArgsConstructor 어노테이션이다.</p>
]]></description>
        </item>
    </channel>
</rss>