<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>swnote_02.log</title>
        <link>https://velog.io/</link>
        <description>왕초보 학부생</description>
        <lastBuildDate>Tue, 25 Feb 2025 07:01:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>swnote_02.log</title>
            <url>https://velog.velcdn.com/images/swnote_02/profile/247bf30a-f0dc-4862-81f7-f34c6d7b30d7/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. swnote_02.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/swnote_02" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Redis저장소를 이용한 토큰 보안성 증가]]></title>
            <link>https://velog.io/@swnote_02/Redis%EC%A0%80%EC%9E%A5%EC%86%8C%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%86%A0%ED%81%B0-%EB%B3%B4%EC%95%88%EC%84%B1-%EC%A6%9D%EA%B0%80</link>
            <guid>https://velog.io/@swnote_02/Redis%EC%A0%80%EC%9E%A5%EC%86%8C%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%86%A0%ED%81%B0-%EB%B3%B4%EC%95%88%EC%84%B1-%EC%A6%9D%EA%B0%80</guid>
            <pubDate>Tue, 25 Feb 2025 07:01:33 GMT</pubDate>
            <description><![CDATA[<h2 id="reids">Reids?</h2>
<p><code>redis</code>는 인메모리 저장소로 NO-SQL방식의 저장소이다.
접근 속도가 빠르고 key-value로 이뤄진 Map형태를 가지고 있기 때문에 탐색이 용이하여 데이터 관리가 편하다는 장점이 있다.</p>
<hr>
<h2 id="redis를-이용한-보안관리">Redis를 이용한 보안관리</h2>
<p>기존에 LocalStroge에서 JWT토큰을 관리하게 되면 로그인을 한 사용자 스토리지를 확인하면
JWT를 그대로 알 수 있다는 보안상 문제가 존재한다.</p>
<p>Access토큰은 외부로 유출되어도 유지시간이 짧기 때문에 보안상 크게 문제가 되지 않지만, 문제는 Refresh 토큰이다. </p>
<p>리프레시 토큰이 탈취당하게 되면 Access토큰을 무한으로 생성할 수 있기 때문에 보안상의 큰 위협이 된다.
<img src="https://velog.velcdn.com/images/swnote_02/post/f8ca52a0-0334-4ca8-8db2-7681d68d8f72/image.png" alt=""></p>
<p>이런 형태로 작동하는데 중간에서 탈취당하게 되면 보안상 막을 수 없어진다.</p>
<p>따라서 Accesstoken은 그대로 사용하되 Refresh 토큰은                                  Redis를 이용한 토큰 저장소에 저장하고 id를 외부로 노출시키는 방법을 사용하면 외부로의 토큰 유출을 막을 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/ee5dd6fc-83f7-43bc-8a2c-c1ae98519c5a/image.png" alt=""></p>
<p>이런 인증 절차를 거치게 해서 보안을 강화할 수 있다.</p>
<hr>
<h2 id="redis-저장소와-로그아웃">Redis 저장소와 로그아웃</h2>
<p>Redis를 사용하면 로그아웃 기능도 구현할 수 있게 된다.</p>
<p>기본적으로 서버에서 로그아웃을 하면 스토리지에 있는 토큰 정보는 지워지게 된다.
하지만 Refresh Token이 탈취된 상태로 지워지게 되면 헤더에 토큰 정보를 담아 로그인 시도를 할 수 있게 된다.</p>
<p>그래서 ID를 저장한 Redis저장소의 토큰을 기한 만료 시키거나 삭제해준다면 Refresh토큰을 이용한 로그인 시도를 방지할 수 있다.
<img src="https://velog.velcdn.com/images/swnote_02/post/68d5d13a-274b-4181-9a44-cba15504b597/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT + SpringSecurity를 이용한 웹페이지 보안 파이프라인 구성]]></title>
            <link>https://velog.io/@swnote_02/JWT-SpringSecurity%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%B3%B4%EC%95%88-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@swnote_02/JWT-SpringSecurity%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%B3%B4%EC%95%88-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Tue, 25 Feb 2025 05:36:45 GMT</pubDate>
            <description><![CDATA[<h2 id="springsecurity-pipeline">SpringSecurity PipeLine</h2>
<p>SpringSecurity에서 제공하는 보안 기능은 기본적으로 세션의 형태로 작동하게 된다. 또한 기본 로그인 폼과 기초 인증, 인가를 제공하기 때문에 보안적으로 사용자가 신경써야할 점을 많이 대신해주게 된다.</p>
<p>하지만 JWT토큰 인증 방식을 사용하기 위해선 Stateless상태로 구성해야하기 때문에 파이프라인을 조금 손봐야 한다.</p>
<p>이번에는 파이프라인에 JWT인증을 추가하는 방법과 구성시 세팅을 알아보도록한다.</p>
<hr>
<h2 id="기본적인-springsecurity-pipeline">기본적인 SpringSecurity pipeline</h2>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/f910e7c0-ce14-450d-81c2-6d2f24475c7c/image.png" alt=""></p>
<p>위 사진의 형태로 파이프라인이 제공되게 된다. </p>
<p>잘 보면 <code>UsernamePasswordAuthentiactionFilter</code>이라는 객체가 있는데, 이 객체가 인증, 인가요청을 담당하게 된다.
인증을 하고 그 정보를 <code>SecurityContextHolder</code>라는 요청을 저장하는 공간에 담아 인가요청을 처리하게 된다.</p>
<p>코드로 보면 아래와 같다.</p>
<pre><code class="language-java">UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                            new UsernamePasswordAuthenticationToken(
                                    userDetails,
                                    null,
                                    userDetails.getAuthorities()
                            );
                    log.info(&quot;■ 접근 토큰 생성 완료&quot;);
                    //접근 토큰 활성화
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                    log.info(&quot;■ 접근 토큰 활성화: &quot;+usernamePasswordAuthenticationToken.getAuthorities());</code></pre>
<p>사용하는 코드의 일부를 가져왔는데, SecurityContextHoler에 <code>setAuthentication</code>메서드를 통해 등록해야하는 <code>UsernamePasswordAuthenticationToken</code>을 등록해 유저가 파이프 라인을 지나갈 수 있는 통행증을 만든다.</p>
<p>이후 session을 통해 관리하게 되고 세션이 유지되면 인증, 인가 요청이 들어올 때 마다 파이프라인을 통과시키게 된다.</p>
<hr>
<h2 id="jwt-토큰을-이용한-파이프라인-설정">JWT 토큰을 이용한 파이프라인 설정</h2>
<p>JWT토큰을 이용하게 되면 아래와 같은 기능이 필요가 없어진다.</p>
<ul>
<li><code>ContextHolder</code>을 이용한 세션유지 처리</li>
<li>기본적인 로그인 폼</li>
</ul>
<p>세션유지는 stateless 형태로 작용하기 때문에 그런 것이고 로그인 폼은 기본적인 로그인 폼을 사용하면 파이프라인에 들어올때 Serucrityfilter가 먼저 들어오게 되는데 아래에 나올 내용이지만 이렇게하면 JWT인증 요청이 두번 발생할 수 있다.</p>
<p>JWT인증 요청 구조는 아래와같다.</p>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/387e5826-1a63-4f2a-a7dc-e7251518e502/image.png" alt=""></p>
<p>이런 형식으로 필터를 구성하게 되면 Spring Security에 들어오기 전에 jwt인증 요청을 해서 원하는 로직으로 JWT토큰을 발급할 수 있다.</p>
<hr>
<h2 id="오류--jwt인증-요청이-두번-발생함">오류 : JWT인증 요청이 두번 발생함.</h2>
<p>파이프라인을 구성할 때 아래와 같이 JWT필터를 넣어주었다.</p>
<pre><code class="language-java">http.addFilterBefore(new jwtRequestFilter(jwtBuilder,userDetailsService), UsernamePasswordAuthenticationFilter.class);</code></pre>
<p><code>jwtRequsetFilter</code>라는 jwt인증요청을 처리하는 필터를 만들어서 <code>UsernamePasswordAuthentication</code>필터 앞에 넣도록 하였다. 저게 바로 Springboot Security에서 진입점에 해당하는 필터이다.</p>
<p>근데 이렇게 하면 문제가 발생한다. 인증이 두번 요청이 되서 처리는 제대로 되는데 로그를 보면 JWT토큰을 두번 인증하게 된다.</p>
<p>문제점은 new로 새로운 의존성을 주입해 객체를만들게 되는데
JWT인증 구조를 보면 아래와 같은 클래스를 상속하고 있는 것을 볼 수 있다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
@Slf4j
public class JwtRequestFilter extends OncePerRequestFilter</code></pre>
<p><code>OncePerRequestFilter</code> 클래스는 해당 필터가 반드시 한번만 동작하도록 보장하는 필터이다.</p>
<p>그럼 두번 동작을 하면 안되는데, 왜 두번 인증을 요청할까?
그건 SpringBoot Bean등록 방식을 봐야한다.</p>
<p>SpringBoot에서 빈을 등록할 때 우리는 어노테이션을 이용해서 Service,Contorller등의 빈을 등록한다.
이 과정은 컴파일 시간에서 이뤄지고 <strong>같은 빈은 등록되면 오류가 나기 때문에 반드시 하나의 빈만 등록된다.</strong> 하지만 security를 사용하게 되면 Security내부적으로 빈을 등록하는 절차를 거치게 된다.</p>
<p>여기서 문제가 발생한다. 
Spring에서 등록한 JWT필터 빈을 Seucrity에서 new로 새로운 필터를 넣었기 때문에 
Spring에서 빈을 등록하고 Security에서 빈을 또 등록해서 이중 인증이 되는 것이다.
원래는 같은 빈은 등록이 되지 않지만 Security에서 등록하기 때문에 컴파일 단계에서 걸러지지 않는것 같다.</p>
<p>해결 방법은 간단하다 코드를</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Slf4j
public class JwtRequestFilter extends OncePerRequestFilter</code></pre>
<p>이렇게 수정하면 Spring에서 빈등록을 하지 않게 된다.
JwtRequest필터는 Security에서만 사용하기 때문에 등록되지 않은 빈을 사용하는 오류가 날 위험도 없다.</p>
<p>따라서 중복문제를 해결할 수 있다.
그리고 새로운 객체를 new를 이용해서 만들고 필터에 넣으면 코드를 보기 힘들 수 있기 때문에</p>
<pre><code class="language-java">http.addFilterBefore(jwtRequestFilter(jwtBuilder,userDetailsService), UsernamePasswordAuthenticationFilter.class);</code></pre>
<p>이런식으로 생성자를 주입한 클래스를 위에서 적용하고 필터 순서를 정할 때 주입된 클래스를 활용하는 것이 안전할 듯 하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT를 이용한 로그인]]></title>
            <link>https://velog.io/@swnote_02/JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@swnote_02/JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Sat, 01 Feb 2025 06:08:22 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-로그인">📌 로그인</h2>
<p>로그인을 하면 JWT토큰을 발행해서 헤더에 담아서 반환하는 것을 구현하였다.</p>
<hr>
<h3 id="controller">Controller</h3>
<pre><code class="language-java">private final signinService service;
    private final JwtBuilder jwtBuilder;
    @PostMapping
    public ResponseEntity&lt;ResponseJson&lt;Object&gt;&gt; signin(
            @RequestBody RequestSignincheckDto signincheckDto,
            HttpServletResponse res
            ){
        String jwtToken = null;
        ResponseSignincheckDto response = service.loginCheck(signincheckDto);
        if(response.getState().value){
            jwtToken = jwtBuilder.generateAccessToken(response.getName());
            res.setHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);
            res.setHeader(&quot;Authorization&quot;, &quot;Bearer &quot; + jwtToken);
        }
        return ResponseEntity.ok(
                ResponseJson.builder()
                        .status(200)
                        .message(&quot;JWT&quot;)
                        .result(res.getHeader(&quot;Authorization&quot;))
                        .build()
        );
    }</code></pre>
<p>Dto는 아이디와 비밀번호를 받는 것과, 로그인이 완료되면 반환받는 객체 두개로 나누었다.</p>
<p>여기서 <code>HttpServletResponse</code>라는게 나오는데 
웹에서 우리가 어떤 요청을 받고 그걸 반환할 때 웹에 있는 헤더 혹은 바디에 반환할 수 있게 해주는 것이다. 여기서는 JWT토큰을 헤더에 담아 반환하기 위해서 사용되었다.</p>
<p>로그인 체크를 이용해 만약 정상적인 유저임이 판단되면 JWT토큰을 생성해서 헤더에 넣어준다. 그리고 헤더에 넣은 값을 확인을 위해 Json으로 반환하도록하였다.</p>
<hr>
<h3 id="service">Service</h3>
<pre><code class="language-java"> private final authRepository repo;

    @Description(&quot;로그인 check&quot;)
    public ResponseSignincheckDto loginCheck(RequestSignincheckDto dto) {
        String id = dto.getId();
        String pw = dto.getPw();
        SignUp valid = repo.findByEmailID(id);
        if(valid!=null){
            if(valid.getPassword().equals(pw)){
                return ResponseSignincheckDto.builder()
                        .state(State.OK)
                        .name(valid.getUserName())
                        .build();
            }
        }
        return ResponseSignincheckDto.builder()
                .state(State.NULL)
                .build();
    }</code></pre>
<p> Service이다. 로그인 채크 기능을 넣어서 <code>authReposiroty</code> Jpa로 탐색을 해서 있는 아이디라면 로그인이 가능하도록 하였다. <code>State</code>를 넣어서 반환하게 하는데, State는 단순히 불린 값으로 해도 상관없지만 보는 사람의 가독성을 위해서 Enum을 하나 정의해서 OK, Null등의 값을 지정하였다.
 그리고 그 값을 불린 값으로 value받을 수 있도록 하여 컨트롤러에서 처리하도록 하였다.</p>
<hr>
<h2 id="repository">Repository</h2>
<pre><code class="language-java">public interface authRepository extends JpaRepository&lt;SignUp,Long&gt; {
    SignUp findByEmailID(String id);
}</code></pre>
<p>이전에 한번 썼던 리포지토리인데, Jpa에서 제공하는 메서드형 쿼리는 findBy~라고 시작하는 것 뒤에 칼럼을 적으면 대상을 탐색해 준다.
따라서 아이디를 탐색할 수 있도록 만든 것이다.</p>
<hr>
<h2 id="jwt">JWT</h2>
<pre><code class="language-java">@Component
public interface JwtBuilder {
    String generateJWT(String name,Long exptime);
    String generateAccessToken(String name);
    String generateRefreshToken(String name);
    JwtCode validateToken(String token);
}</code></pre>
<p>컨트롤러에서 사용하는 JWT빌더의 인터페이스이다. <code>@Component</code>를 이용해서 스프링 빈에 등록될 수 있도록 하였고, 사용할 메서드를 등록해주었다.</p>
<pre><code class="language-java">@Component
public class JwtBuilderImpl implements JwtBuilder {
    @Value(&quot;${jwt.secret.key}&quot;)
    private String SecretKey; //키값임.
    private static final Long AccessTokenExpTime = 1000*60L*3L;
    private static final Long RefreshTokenExpTime = 60L * 1000 * 60L;
    public String generateJWT(String name,Long exptime){
        Map&lt;String,Object&gt; header = new HashMap&lt;&gt;();
        header.put(&quot;typ&quot;, &quot;JWT&quot;); //토큰 헤더 설정

        Date ext = new Date();
        ext.setTime(ext.getTime()+exptime); //유효시간 설정

        Map&lt;String,Object&gt; payload = new HashMap&lt;&gt;();
        payload.put(&quot;user_name&quot;,name);//토큰 페이로드설정

        String jwt = Jwts.builder()
                .setHeader(header)
                .setClaims(payload)
                .setSubject(&quot;test&quot;)
                .setExpiration(ext)
                .signWith(SignatureAlgorithm.HS256,SecretKey.getBytes())
                .compact();
        return jwt;
    }</code></pre>
<p>재정의한 Impl이다. 키값을 정하는 <code>SecretKey</code>는 Value어노테이션으로 application.properies에 
<code>jwt.secret.key</code> 이런 식으로 정의한 것을 가져올 수 있게 하였다.
토큰은 유지시간이 있어야 보안적으로 안정적이기 때문에 어세스, 리프레시 토큰의 유지시간을 설정해 주었다.
그리고 헤더, 페이로드 등을 설정한 후에 
<code>jwts</code>의 빌더를 통해  등록해주고 마지막에 signWith에 등록한 키와 알고리즘을 넣어주면
토큰 값을 얻을 수 있다.</p>
<pre><code class="language-java">public String generateAccessToken(String name){
        return generateJWT(name,AccessTokenExpTime);
    }
    public String generateRefreshToken(String name){
        return generateJWT(name,RefreshTokenExpTime);
    }</code></pre>
<p>실제로 사용할 토큰 발급 메서드이다. AccessToken과 RefreshToken을 구분해서 생성해야 하기 때문에 이렇게 메서드를 나눠서 발급 시간을 조정하였다.</p>
<pre><code class="language-java">    public JwtCode validateToken(String token) {
        if (token == null || token.trim().isEmpty()) {
            return JwtCode.DENIED; // 토큰이 유효하지 않음
        }
        try {
            Jwts.parserBuilder().setSigningKey(SecretKey).build().parseClaimsJws(token);
            return JwtCode.ACCESS;
        } catch (ExpiredJwtException e) { // 기한 만료
            return JwtCode.EXPIRED;
        } catch (Exception e) {
            return JwtCode.DENIED;
        }</code></pre>
<p>토큰을 검증하는 메서드이다. 나중에 어떤 페이지에 접근하건 헤더에 있는 토큰을 검증해서 사용해야할텐데 그 검증을 도와주는 메서드이다. </p>
<hr>
<p>이제 이걸HTTP에 날려서 확인해보면
<img src="https://velog.velcdn.com/images/swnote_02/post/d3a0d48c-2fce-437f-88b5-ba0299a61db8/image.png" alt=""></p>
<p>이렇게 POST요청을 날려주면
<img src="https://velog.velcdn.com/images/swnote_02/post/7def715c-373a-4662-81e1-1c5b9b6268ed/image.png" alt=""></p>
<p>이런 식으로 요청이 반환 되는 것을 볼 수 있다.
이걸 <a href="https://jwt.io">https://jwt.io</a> 사이트에서 디코딩을 해볼 수 있는데,</p>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/09162c5a-da2b-4e69-aa37-ecd111bbd64c/image.png" alt=""></p>
<p>이런 결과를 얻을 수 있다. 헤더와 페이로드는 알 수 있지만 가장 중요한 사인은 디코딩을 해도 알 수 없는 모습이다. 이렇게 페이지 보안을 강화할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Error: Could not find or load main class worker.org.gradle.process.internal.worker.GradleWorkerMain]]></title>
            <link>https://velog.io/@swnote_02/Error-Could-not-find-or-load-main-class-worker.org.gradle.process.internal.worker.GradleWorkerMain</link>
            <guid>https://velog.io/@swnote_02/Error-Could-not-find-or-load-main-class-worker.org.gradle.process.internal.worker.GradleWorkerMain</guid>
            <pubDate>Tue, 28 Jan 2025 13:35:52 GMT</pubDate>
            <description><![CDATA[<p>해결한지 좀 된 오류인데, 해결할 당시 이것저것 다 해봤는데 안되는 경우가 있어서
따로 작성한다.</p>
<p>먼저 이 오류는 잘 되다가 갑자기 발생했었다.
git으로 여러 컴퓨터와 연결해서 사용하고 있었는데, Test파일을 수정한 후에 push를 하고
다른 컴퓨터에 가서 patch를 하니까 오류가 발생했다.</p>
<p>총 4가지의 오류가 발생했는데 다 비슷한 문구를 가지고 있었다.
<code>Error: Could not find or load main class worker.org.gradle.process.internal.worker.GradleWorkerMain</code>
이건 뭘까 해서 Gpt한테 물어봤더니</p>
<ol>
<li>플러그인 버전 확인</li>
<li>Spring 플러그인 저장소 추가</li>
<li>Gradle 캐시 정리 및 다시 다운로드</li>
<li>Gradle 버전 확인</li>
<li>Gradle Wrapper 사용</li>
</ol>
<p>이런 해결책을 내놓았다.
해서 다 해봤는데 당연히 안됬고 회사에 가서 사수님께 물어봤더니
경로 확인을 해보라고 하셨다.
문제는 거기에 있었는데
<img src="https://velog.velcdn.com/images/swnote_02/post/eec78127-b170-420e-9c51-34fd6b27782e/image.png" alt=""></p>
<p>이렇게 프로젝트 경로가 영어로 되어있어야 하는데,
어느 곳이던 한글 경로가 들어가 있으면 오류가 발생하였다.
한글을 바꿔주니까 잘 작동하였다.</p>
<p>추가로 이런 오류의 대부분은 그래들 문제가 아니고 파일 설정에 관한 문제라고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회원가입]]></title>
            <link>https://velog.io/@swnote_02/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-JWT-%ED%86%A0%ED%81%B0%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@swnote_02/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-JWT-%ED%86%A0%ED%81%B0%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Tue, 28 Jan 2025 13:13:24 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-회원가입">📌 회원가입</h2>
<p>가입하는 방식은 아이디와 비밀번호, 사용자 이름을 입력하면 사용자 이름을 반환하도록 하였다.
DB에서 PK가 사용자 이름으로 되어있기 때문에 중복된 사용자는 가입을 해도 DB가 변하지 않는다,
나중에 추가적으로 오류를 발생시키는 코드를 넣고 이번에는 그냥 가입만 진행할 수 있도록 한다.</p>
<hr>
<h3 id="controller">Controller</h3>
<pre><code class="language-java">@Description(&quot;회원가입&quot;)
    @PostMapping(&quot;/api/sign-up&quot;)
    public ResponseEntity&lt;ResponseJson&lt;Object&gt;&gt; signUp(@RequestBody RequestSignupPostDto requestSignupPostDto){
        ResponseSavedNameDto response = service.signup(requestSignupPostDto);

        return ResponseEntity.ok(
                ResponseJson.builder()
                        .status(200)
                        .message(&quot;OK&quot;)
                        .result(response)
                        .build()
        );
    }</code></pre>
<p> 회원가입을 진행하기 위해 사용할 컨트롤러이다.
 <code>RequsetSignupPostDto</code>는 사용자에게 요청을 하기 위한 필드이다. Dto안에 있는 필드로 요청을 받을 수 있게 된다.
 <code>ResponseSvedNameDto</code>는 가입한 사용자 이름을 반환하기 위한 Dto이다. Dto의 목적은 반환하는 것와 요청하는 Dto를 다르게 해야 그 의미가 있는것이라. (한가지 역할만 해야한다) Request요청을 처리하는 것과 Response요청을 처리하는 것은 나눠야 한다.</p>
<p>그렇게 받은 데이터를 Json의 형태로 만들어 보내기 위해 <code>ResponseJson</code>이라는 것을 만들어 보내도록 하였다.</p>
<hr>
<h3 id="dto">DTO</h3>
<pre><code class="language-java">public class RequestSignupPostDto {
    private String email;
    private String password;
    private String userName;
}

public class ResponseSavedNameDto {

    private String userName;
}
public class ResponseJson&lt;T&gt; {
    Integer status;
    String message;
    T result;

    @JsonIgnore
    Object trace;
    @JsonIgnore
    Object path;
}</code></pre>
<p>Json은 Dto는 아닌데 일단 그냥 넣어놨다. 
Dto는 반드시 받을 데이터의 필드만 존재해야 하기 떄문에 타입과 필드명만 지정해주고 나머지는 쓰지 않아야 한다. Dto,Dao,VO의 구분을 확실히 해야한다.</p>
<hr>
<h3 id="service">Service</h3>
<pre><code class="language-java">public class signupService {

    private final authRepository signInRepo;

    @Description(&quot;회원 가입&quot;)
    public ResponseSavedNameDto signup(RequestSignupPostDto request) {
        SignUp signUp = SignUp.builder()
                .emailID(request.getEmail())
                .password(request.getPassword())
                .userName(request.getUserName())
                .build();
        SignUp savedSignUp = signInRepo.save(signUp);
        return ResponseSavedNameDto.builder()
                .userName(savedSignUp.getUserName())
                .build();
    }
}</code></pre>
<p>위 코드에서는 좀 빠진부분이 있는데 service라고 정의된 부분이다.
Sevice단에서는 엔티티를 이용해서 DB에 커넥션하고 데이터를 수정하는 역할을 한다.
요청으로 받은 RequsetDto를 SignUp엔티티에 적용해서 DB에 적용시킨다.
DB에 적용하는 것은 JPA를 이용하였다.
그리고 반환된 이름을 다시 ResponseDto에 넣어서 컨트롤러로 반환해준다.</p>
<hr>
<h3 id="entity">Entity</h3>
<pre><code class="language-java">@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SignUp {
    @Id
    @Column(length = 20)
    private String emailID;

    @Column(length = 10)
    private String password;

    @Column(length = 10)
    private String userName;

    @CreatedDate
    private LocalDateTime signInDate;


}</code></pre>
<p>엔티티이다. 엔티티는 반드시 DB와 일치하는 컬럼을 가지고 있어야 한다. 따라서 수정 또한 자유롭게 두면 안된다.
그래서 위에 어노테이션 설정한 것을 보면 Setter가 없다는 것을 알 수 있다. 빌더를 통하지 않고 Setter로 하게되면 잘못된 데이터를 넣을 가능성이 있기 때문에 수정을 제한하는 것이다.</p>
<hr>
<h3 id="repository">Repository</h3>
<pre><code class="language-java">public interface authRepository extends JpaRepository&lt;SignUp,Long&gt; {
    SignUp findByEmailID(String id);
}</code></pre>
<p>리포지토리이다. JPA를 이용하고 있으며 JPA에 대한 것은 다른 포스트에 정리했기 때문에 넘어가겠다.
각 엔티티로 반환하는 메서드쿼리를 날리면 그에 따른 응답을 받을 수 있다.</p>
<p>물론 저 <code>findByEmailID</code>를 여기서는 사용하지 않았다 그냥 필요해서 나중에 쓸라고 추가한 것이고</p>
<pre><code class="language-java">SignUp savedSignUp = signInRepo.save(signUp);`</code></pre>
<p>이게 회원가입을 할때 Save를 할 수 있는 기능이다.
엔티티에 정보를 담고 그걸 다시 저장된 엔티티로 반환하는 개념이다.</p>
<hr>
<h3 id="http">HTTP</h3>
<p>인텔리제이에서는 HTTP를 날려볼 수 있는 기능을 지원한다.
<img src="https://velog.velcdn.com/images/swnote_02/post/1fe7ae0e-a882-4be6-9d8e-1d777c2f722f/image.png" alt=""></p>
<p>PostMapping을 눌러보면 이렇게 HTTP요청을 날릴 수 있는 것이 나온다.
<img src="https://velog.velcdn.com/images/swnote_02/post/1f522415-aafc-4bff-9669-4254e2a04860/image.png" alt="">
그럼 이런식으로 POST요청을 날릴 수 있는 기능을 지원하고 HTTP의 메서드는 다 해볼 수 있다.</p>
<p>이 요청을 날려보면
<img src="https://velog.velcdn.com/images/swnote_02/post/7b5ef361-ec9f-4e23-9940-380ab112aba3/image.png" alt=""></p>
<p>이렇게 정상적으로 요청이 처리된 것을 볼 수 있다.</p>
<p>DB에서 확인해보면
<img src="https://velog.velcdn.com/images/swnote_02/post/a140355b-8426-4b06-a283-83dfd94b770b/image.png" alt=""></p>
<p>정상적으로 DB에도 들어온 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[API]]></title>
            <link>https://velog.io/@swnote_02/API</link>
            <guid>https://velog.io/@swnote_02/API</guid>
            <pubDate>Mon, 27 Jan 2025 09:18:46 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-api">📌 API</h2>
<p>웹에서 프론트엔드와 벡엔드를 나눠서 개발하기 전에 필수적으로 알아야할 개념이라고 생각해서 정리한다.</p>
<hr>
<h2 id="apiapplication-programming-interface">API(Application Programming Interface)</h2>
<p>애플리케이션간의 통신 방법을 정의하는 것을 의미한다.
주로 요청과 응답으로 이루어져 있으며, 클라이언트와 서버 관계에서 웹 API또는 RestAPI로 많이 사용된다.</p>
<p>예전에는 템플릿 언어로 구성된 화면에서 파라미터를 통해 화면으로 보내는 View방식을 사용했지만 요즘은 벡단과 프론트단으로 나눠서 하기 때문에, 프론트단에서 백단으로 데이터를 Requset하고 백단에서는 요청받은 데이터를 가공해서 프론트단으로 Response해야한다.</p>
<p>이때 서로 규칙을 정해야하는데, 이게 바로 API이다.</p>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/ea57a161-9519-4440-9254-c94c46ea69fc/image.png" alt=""></p>
<p>이런 방식으로 웹이 통신하게 된다.</p>
<hr>
<h2 id="ui">UI</h2>
<p>사용자가 브라우저에서 접하는 화면이다. 사용자의 행위에 의해 이벤트가 발생하고, 서버로 요청이 들어간다. 전통적인 방식은 HTML을 요청해서 화면을 구성하지만 SPA(Vue, Angular, React)는 클라이언트에서 렌더링하고 API를 호출하게 된다.</p>
<hr>
<h2 id="웹애플리케이션">웹애플리케이션</h2>
<p>HTML,Css,JAVAScript등이 여기에 속한다. 서버 렌더링인 경우 JSP를 이용해서 REST API를 호출하고 벡엔드로부터 전달받은 JSON데이터로 구성한다. 클라이언트 렌더링에서는 웹애플리케이션 서버 내부 파일시스템 대신 버킷 등의 오브젝트 저장소를 사용해서 Static Web을 구성한다.</p>
<hr>
<h2 id="📌-rest-api">📌 REST API</h2>
<p>Rest를 기반으로 만들어진 API이다.</p>
<h3 id="restrepresentational-state-transfer">REST(Representational State Transfer)</h3>
<p>자원의 이름을 구분해서 해당 자원의 상태를 주고 받는 모든 것</p>
<ol>
<li>HTTP URI를 통해 자원을 명시</li>
<li>HTTP Method(POST,GET등)을 통해</li>
<li>해당 자원에 대한 CRUD Operation을 적용한다.</li>
</ol>
<p>쉽게 말하면 HTTP 프로토콜을 통해 수정사항을 DB에 반영할 수 있게되는 것이다.</p>
<h3 id="rest-api응답에-포함된-요소">REST API응답에 포함된 요소</h3>
<p>1) 상태 표시줄
여러가지 상태 코드가 반환되는데 각 코드는 의미를 담고있다.</p>
<ul>
<li>200 OK 일반 성공 응답</li>
<li>201 Created POST 성공 응답</li>
<li>400 BadRequest 서버가 처리할 수 없는 잘못된 요청</li>
<li>500  Internal Server Error 서버에 문제가 있음</li>
<li>503 Service Unavailable 서버가 요청을 처리할 준비가 되지 않았음</li>
</ul>
<p>이외에도 여러가지 코드가 있다.</p>
<p>2) 메시지 본문
헤더에 포함된 내용을 기반으로 적절한 표현형식을 선택하여 리소스를 표현한다.
주로 JSON형식을 반환한다.</p>
<p>3) 헤더
응답에 대한 헤더 또는 메타데이터를 포함한다. 서버, 인코딩, 날짜 콘텐츠 유형같은 정보를 포함한다.</p>
<h3 id="rest-api인증">REST API인증</h3>
<p>RESTful 웹서비스는 응답을 보내기 전에 먼저 요청을 인증해야한다.</p>
<p>1) HTTP인증
기본 : 클라이언트는 요청 헤더에 사용자 이름과 암호를 넣어 전송한다.
전달자(Bearer)인증 : 토큰 전달자에 대한 엑세스 제어를 제공한다. 일반적으로 전달자 토큰은 서버가 로그인 요청에 대한 응답으로 생성하는 암호화 된 문자열이다. 주로 JWT를 사용한다.</p>
<p>2) API키
REST API를 인증하기 위한 또다른 옵션이다. 서버는 고유하게 생성된 값을 최초 클라이언트에 할당하고 클아이언트는 리소스에 엑세스 하려고 할 때마다 고유한 API키를 사용한다.</p>
<p>3) OAuth 모든 시스템에 대한 안전한 로그인 프로세스를 보장하기 위해 암호와 토큰을 결합한다. 나중에 한번 다시 정리할거긴 한데, OAuth2.0에 대한 글을 작성할 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RestController와 Controller]]></title>
            <link>https://velog.io/@swnote_02/RestController%EC%99%80-Controller</link>
            <guid>https://velog.io/@swnote_02/RestController%EC%99%80-Controller</guid>
            <pubDate>Mon, 27 Jan 2025 08:13:11 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-문제">📌 문제</h2>
<p>스프링 부트에서 사용하는 어노테이션 중에 Controller단에서 사용하는 RestController와 Controller가 있다. 문제의 발생은 html 파일을 열어야 하는 Controller단에서 html파일을 어떻게 해도 불러올 수 없길래 찾아보게 되었다.</p>
<hr>
<h2 id="📌-controller">📌 Controller</h2>
<p>간단하게 말하면 <strong>MVC 패턴에서 사용하는 View를 불러오는 방법</strong> 이라고 표현할 수 있을 것 같다.
Controller는 기본적으로 view를 띄우는 것에 활용하게 된다. 
resource폴더에서 html파일을 띄워서 직접적으로 연결해서 활용하는 방법을 사용하게 되는데,
요즘은 벡엔드와 프론트엔드를 나눠서 json으로 데이터만 전달하는 방식을 활용하고 있기 때문에 view를 따로 연결하지 않는다는 이유로 잘 사용하지 않는다.</p>
<p>controller은 <code>@controller</code> 와 <code>@ResponseBody</code>를 사용해서 페이지가 아니라 응답값을 그대로 반환하기 위해서 사용하게 된다.
json으로 데이터를 넘겨주기 위해 두가지 어노테이션을 사용해야한다.</p>
<hr>
<h2 id="📌-restcontroller">📌 RestController</h2>
<p>간단하게 <strong>Controller과 ResponseBody를 합친 것</strong>이라고 생각하면 된다. </p>
<p>벡엔드와 프론트엔드가 나눠서 작업을 하기 때문에 데이터만 주고받으면된다.
따라서 페이지를 띄우는 Controller방식을 사용할 필요가 없어졌기 때문에 RestConstroller방식을
사용하게 되었다.</p>
<p>다음 포스트에서는 <code>Restcontroller</code>을 어떻게 사용하는지 프론트엔드와 벡엔드 사이에서 구동하는 방식에 대해서 알아봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드 리팩토링(2)]]></title>
            <link>https://velog.io/@swnote_02/%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%812</link>
            <guid>https://velog.io/@swnote_02/%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%812</guid>
            <pubDate>Wed, 22 Jan 2025 11:59:38 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-코드-리뷰">📌 코드 리뷰</h2>
<p>지난 포스트에서 아이디어를 잘 작성했는데, 코드를 작성하는데 있어서 문제가 없진 않았다.
해결책과 동시에 코드를 첨부한다.</p>
<hr>
<h2 id="📌-코드">📌 코드</h2>
<pre><code class="language-java"> public void makeTreeNode() {
        while (!priorityQueue.isEmpty()) {
            CategoryTest categoryTest = priorityQueue.poll();
            if (categoryTest.getDepth() == 0) { // Root node
                TreeNode&lt;String, CategoryTest&gt; root = new TreeNode&lt;&gt;(categoryTest.getSeq().toString(), categoryTest.getName(), categoryTest);
                unionAll(root);
                categoryTree = new Tree&lt;&gt;(root);
                break;
            }
            if (contains(categoryTest.getSeq().toString())) {
                unionTreeNode(categoryTest);
            } else {
                insertOrAddToMap(categoryTest);
            }
        }
    }</code></pre>
<p>메인 코드는 이렇게 동작한다 하나하나씩 살펴보면</p>
<pre><code class="language-java">while (!priorityQueue.isEmpty()) {
CategoryTest categoryTest = priorityQueue.poll();</code></pre>
<p>큐에서 모든 자료를 다 꺼낼 때 까지 반복하도록 한다.</p>
<pre><code class="language-java">if (categoryTest.getDepth() == 0) { // Root node
                TreeNode&lt;String, CategoryTest&gt; root = new TreeNode&lt;&gt;(categoryTest.getSeq().toString(), categoryTest.getName(), categoryTest);
                unionAll(root);
                categoryTree = new Tree&lt;&gt;(root);
                break;
            }</code></pre>
<p>depth를 검사했을 때 0이라면 최상위 노드이기 때문에 root노드를 만들어서 최상위 노드를 만들어준다.</p>
<pre><code class="language-java">public void unionAll(TreeNode&lt;String, CategoryTest&gt; root) {
        LinkedList&lt;TreeNode&lt;String, CategoryTest&gt;&gt; unionList = map.get(root.getKey());
        root.setChildren(unionList);
    }</code></pre>
<p>사용되는 <strong>unionAll</strong>함수이다. 트리를 만들다 보면 루트 트리 아래에 도달하면(depth가 1) map안에는 
depth가 1인 것 하나만 남아있어야 한다. 그 부분을 구현한 것이다.
<strong>setChildren</strong>을 통해 리스트를 루트에 연결해준다.</p>
<pre><code class="language-java"> if (contains(categoryTest.getSeq().toString())) {
                unionTreeNode(categoryTest);</code></pre>
<p>seq가 이미 map에 키로 포함되어있는 경우이다. 이 경우에는 map에서 리스트를 해당 노드의 자식으로 만들어 줘야 한다.</p>
<pre><code class="language-java">    public void unionTreeNode(CategoryTest categoryTest) {
        TreeNode&lt;String, CategoryTest&gt; node = new TreeNode&lt;&gt;(categoryTest.getSeq().toString(), categoryTest.getName(), categoryTest);
        List&lt;TreeNode&lt;String, CategoryTest&gt;&gt; children = map.get(categoryTest.getSeq().toString());

        if (children != null) {
            node.setChildren(new LinkedList&lt;&gt;(children)); // Copy and set children
        }
        map.computeIfAbsent(categoryTest.getParentSeq().toString(), k -&gt; new LinkedList&lt;&gt;()).add(node);
        map.remove(categoryTest.getSeq().toString()); // Safely remove child data after moving
    }</code></pre>
<p> 그 부분을 구현하는 <strong>unionTreeNode</strong>이다. 해당 노드의 seq값은 자식 노드 그니까 map에 들어간 노드의 parnetSeq값일 것이다. 그것을 이용해서 자식을 찾는다. 이후에 자식이 비어있지 않다면 자식을 부모 노드로 연결을 해주는데, 이때 <strong>새로운 리스트로</strong>연결을 해줘야 한다.
 이유는 참조에 관련된 이야기인데, 아래 코드를 보면
 remove를 통해 원래 있었던 자식의 공간을 지우고 있다. 이 과정에서 깊은 복사가 이뤄지지 않으면 
 복사했던 객체의 참조도 지워지게 되서 자식이 지워지는 경우가 발생한다. 그래서 새로운 객체로 옮겨서
 주소를 옮겨주는 것이다.
이 모든 경우에도 포함되지 않는다면, </p>
<pre><code class="language-java">    public void InsertOrAddToMap(CategoryTest categoryTest) {
        TreeNode&lt;String, CategoryTest&gt; node = new TreeNode&lt;&gt;(categoryTest.getSeq().toString(), categoryTest.getName(), categoryTest);
        map.computeIfAbsent(categoryTest.getParentSeq().toString(), k -&gt; new LinkedList&lt;&gt;()).add(node);
    }</code></pre>
<p>이미 키가 있다면 추가하고 없다면 새로 만들어서 추가하는 메서드이다. <strong>computeIfAbsent</strong> 메서드는 Map에서 제공하는 메서드인데, categoryTest.getParentSeq().toString() 를 찾는 것에 실패한다면 새로운 링크리스트를 만들어서 add하고 성공한다면 getParentSeq를 한 곳에 add하는 메서드이다. 참이면 본인을 반환하고 거짓이라면 뒤를 반환한다.</p>
<pre><code class="language-java">    public Tree&lt;String, DepartmentTest&gt; getTree() {
        makeTreeNode();
        return Tree;
    }</code></pre>
<p>만든 트리를 반환하는 로직이다. 생성자에 makeTreeNode를 넣을수도 있지만 LazyEvaluation을 하기 위해서 getTree에 넣어서 원할때만 트리를 만들도록 하였다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>코드를 짜는 것은 어렵지 않았다. 몇 가지 이슈가 있었지만 코드 내용 자체는 간단했고, 트리를 만드는 것에 한하여 속도 향상 또한 많이 좋아졌다.
이제 이걸 다양한 곳에 사용하기 위한 제네릭 클래스를 만들어볼 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드 리팩토링(1)]]></title>
            <link>https://velog.io/@swnote_02/%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%811-i038mr72</link>
            <guid>https://velog.io/@swnote_02/%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%811-i038mr72</guid>
            <pubDate>Sun, 19 Jan 2025 03:35:21 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-코드-리팩토링">📌 코드 리팩토링?</h2>
<p>코드 리팩토링은 기존 코드의 동작을 바꾸지 않고 구조와 가독성을 개선하는 과정을 말한다.
코드 품질을 높이고 유지보수성을 향상시키는 것에 목적이 있다.</p>
<hr>
<h2 id="📌-리팩토링을-하는-코드와-그-이유">📌 리팩토링을 하는 코드와 그 이유</h2>
<p>코드중에 이런 코드가 있었다.</p>
<pre><code class="language-sql">-- WITH RECURSIVE를 사용하여 재귀 쿼리 작성
WITH RECURSIVE EmployeeHierarchy AS (
    -- Anchor 쿼리: 최상위 직원 (ManagerID가 NULL인 직원)
    SELECT 
        EmployeeID,
        Name,
        ManagerID,
        1 AS Level
    FROM Employees
    WHERE ManagerID IS NULL

    UNION ALL

    -- 재귀 쿼리: 하위 직원 탐색
    SELECT 
        e.EmployeeID,
        e.Name,
        e.ManagerID,
        eh.Level + 1 AS Level
    FROM Employees e
    INNER JOIN EmployeeHierarchy eh
        ON e.ManagerID = eh.EmployeeID
)
-- 결과 조회
SELECT * 
FROM EmployeeHierarchy
ORDER BY Level, EmployeeID;</code></pre>
<p>정확히 원본 코드는 아니지만 최상위 직원을 찾아서 트리를 구성하는 재귀 탐색 SQL탐색문이다.</p>
<p>이렇게 구성하면 SQL을 본래 알던 사람도 다른 사람의 코드를 보면 한번에 이해하기 힘들다는 
단점이 있다. 이는 곧 유지보수의 문제가 있다는 단점으로 갈 수 있기 때문에 좋지 못한 코드가 된다.</p>
<p>그런문제도 있지만 가장 큰 문제점은 재귀 탐색을 하고 있어서 성능의 문제가 있다는 것이다.</p>
<hr>
<h2 id="📌-해결-아이디어">📌 해결 아이디어</h2>
<p>기본적인 아이디어 흐름은 먼저 재귀 탐색을 없애는 것이다.
 SQL에서 재귀탐색을 통해 트리를 구성하고 밖에 나와서 또 재귀탐색을 통해 트리를 내보내는 형태인 코드를 재귀 탐색을 완전히 없애는 것이 목적이다.</p>
<p> -&gt; 쿼리를  이용해서 재귀 탐색을 진행하니까 쿼리를 쓰지 않고 트리를 구성하면 되지 않을까?
 현재 파일 구성은 mapper을 이용해서 DB를 커넥션해서 불러오고 있다. 이 과정에서 Xml파일에 SQL이 담기고 그걸 이용해서 트리를 만들고 있다.</p>
<p> -&gt; 이 방법을 사용하지 않으려면?
 Mapper로 불러오는 방법 대신 다른 방법을 사용해야 한다. -&gt; Jpa를 통해 불러오자</p>
<pre><code class="language-java"> @Repository
public interface TestRepository extends JpaRepository&lt;MyEntity, Long&gt;{
    List&lt;MyEntity&gt; findBySeq(Long Seq);
}</code></pre>
<p>이런 식으로 불러올수 있다. Jpa에 대한 설명은 다른 포스트에 하도록 한다.</p>
<p>여기서 MyEntity는 엔티티 객체이다. 이 엔티티 객체가 곧 DB커넥션을 통해 값을 가져올 수 있는 
중간 매계가 된다.</p>
<p>-&gt; 어떤 방식으로 불러오는것일까? 순서가 어떻게 될까
Jpa를 통해 DB에서 값을 불러오고 Controller에 그 값을 전달한 후에 적절한 처리를 통해 트리를 구성해서 Json으로 반환하면 되겠다.</p>
<p>-&gt; 기준 키가 무엇일까?
이걸 알려면 일단 DB에서 어떻게 데이터를 전달하는지 알아야 했다.</p>
<pre><code class="language-json">{
    &quot;seq&quot;: 719,
    &quot;parentSeq&quot;: 713,
    &quot;companySeq&quot;: 197,
    &quot;depth&quot;: 4,
    &quot;name&quot;: &quot;기술연구소&quot;,
    &quot;status&quot;: &quot;A&quot;,
    &quot;regId&quot;: &quot;string&quot;,
    &quot;regDate&quot;: &quot;2024-08-26T00:42:18.859+00:00&quot;,
    &quot;modId&quot;: &quot;&quot;,
    &quot;sort&quot;: 0
  }</code></pre>
<p>이런 형식으로 불러온다는 것을 알게 되었다.
그럼 아까 Jpa를 통해 depth가 10인 필드부터 불러올 수 있겠다.
-&gt; 근데 이렇게 하면 SQL호출을  해야해서 JPQL을 사용하거나 따로 쿼리문을 작성해서 Mapper을 써야한다.</p>
<p>우리가 원하는 방식은 Jpa를 사용해서 불러오는 것이기 때문에 일단 모든 값을 불러오고
그 값을 정리하는 방식을 써야겠다.</p>
<p>-&gt; 그럼 기준을 어떻게 잡아야 할까?
자세히 보니까 companySeq라는 부분이 모든 json에서 동일한 것을 확인하였다.
따라서 companySeq로 일단 모든 트리 객체를 불러올 수 있을 것 같다.</p>
<p>-&gt; 그럼 모두 뽑은 후에는 어떻게 정렬해야 할까?
트리의 구조를 만드는 것에는 위에서 내려오는 방법도 있지만 아래에서 모든 것을 통합해서 올리는 방법도 
있다. 이 방법을 사용할 것이다.
<strong>우선순위 큐</strong>를 사용해서 Depth를 높은 순(10~0) 순으로 정렬하고 알고리즘을 짜면 되겠다.</p>
<p>-&gt;알고리즘 순서</p>
<ol>
<li>Jpa로 모든 트리 요소를 불러와서 List에 담는다.</li>
<li>우선순위 큐안에 depth가 높은 순서대로 정렬한다.</li>
<li>큐 안에서 하나를 뽑아서 트리노드 하나로 만든다.</li>
<li>그 노드는 depth가 가장 높은 가장 하위 객체일 것임</li>
<li>큐가 빌 때까지 아래 과정을 반복한다.
5-1. depth비교 후같다면
5-2. Map에 키가 존재한다면 이미 등록된것임으로 추가하고
5-3. 키가 존재하지 않는다면 새로운 키임으로 하나를 만들어서 Map에 넣는다.</li>
<li>이런 과정을 반복해서 하나의 depth가 끝난다면 모아놓은 데이터를 모두 부모 객체에 전달하고 부모를 추가한다.</li>
<li>depth가 0이 나오면 모든 객체를 전부 넣어서 트리를 마무리한다.</li>
</ol>
<p>이렇게 쓰면 좀 이해가 어렵다. 요는 아래서부터 트리를 만드는 것인데 그림을 참고하자</p>
<h4 id="부모가-같은-노드를-삽입할-때">부모가 같은 노드를 삽입할 때</h4>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/531de053-9780-4b2d-90c7-94b9d9ba7b9e/image.png" alt=""></p>
<h4 id="부모가-다른-노드를-삽입할-때-depth가-같다면">부모가 다른 노드를 삽입할 때 depth가 같다면</h4>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/3b1668e6-9388-4305-a7d3-ff7a73952b2f/image.png" alt=""></p>
<h4 id="depth가-다른-노드를-삽입할-때">depth가 다른 노드를 삽입할 때</h4>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/7d55d975-0804-4d88-9cc3-2a7c6a938bdc/image.png" alt=""></p>
<hr>
<p>이런 형식으로 작동하게 된다. 일단 아이디어는 이렇고 다음 포스트에서는 코드를 다루겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Annotation 정리]]></title>
            <link>https://velog.io/@swnote_02/Annotation-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@swnote_02/Annotation-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 18 Jan 2025 07:00:31 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-어노테이션">📌 어노테이션?</h2>
<p>어노테이션은 다른 프로그램에게 유용한 정보를 제공하기위해 사용되는 것으로
주석과 같은 의미를 가진다.</p>
<p>사전적 정의는 이러한데, 사실 사용될 때 보면 정말 많은 정보와 기능을 담고 있다.
어노테이션을 사용한 프로그래밍은 유지보수적, 코드 가독성에 정말 많은 도움을 주니까
꼭 알아두도록 하자.</p>
<hr>
<h2 id="📌-어노테이션의-역할과-종류">📌 어노테이션의 역할과 종류</h2>
<p>어노테이션의 역할</p>
<ul>
<li>컴파일러에게 문법 에러를 체크하도록 정보를 제공한다.</li>
<li>프로그램을 빌드할 때 코드를 자동으로 생성할 수 있도록 정보를 제공한다.</li>
<li>런타임에 특정 기능을 실행하도록 정보를 제공한다.</li>
</ul>
<p>어노테이션의 종류
어노테이션은 크게 세 종류로 구분할 수 있다.</p>
<ul>
<li>표준 어노테이션</li>
<li>메타 어노테이션</li>
<li>사용자 어노테이션</li>
</ul>
<h3 id="표준-어노테이션">표준 어노테이션</h3>
<p>자바에서 기본적으로 제공하는 어노테이션이다.
@Override가 대표적인 표준 어노테이션이다.</p>
<h3 id="메타-어노테이션">메타 어노테이션</h3>
<p>어노테이션에 붙이는 어노테이션으로, 어노테이션을 정의하는 것에 사용한다.
이게 대체 무슨말인가 하면 표준 어노테이션의 적용 범위나 주석(스키마)를 단다고 생각하면 된다.</p>
<h3 id="사용자정의-어노테이션">사용자정의 어노테이션</h3>
<p>말 그대로 어노테이션을 붙이면 사용자가 원하는 행동을 할 수 있도록 정의할 수 있게 하는 것이다.
TestCase포스트에서 Valid를 사용하면서 MyNotNull 어노테이션을 만들면서 jakarta 어노테이션을 만든 적이 있었는데, 이런 것이 사용자 어노테이션이다.</p>
<p>장점으로는 표준 어노테이션과 다르게 행동을 정의할 수 있다는 점이 있지만,
행동을 제약하려면 특정한  방식 즉 나름의 프레임 워크를 따라 가야해서 어느정도는 규칙을 지켜야 한다.</p>
<hr>
<h2 id="📌-spring-boot-에서-많이-쓰는-어노테이션">📌 Spring Boot 에서 많이 쓰는 어노테이션</h2>
<p>아래는 스프링 부트에서 정말 많이 사용되는, 그리고 자바에서도 많이 사용되는 어노테이션들이다.
점점 알게 될 수록 추가할 예정이다.</p>
<h4 id="validated">@Validated</h4>
<p>메서드 파라미터의 검증을 위해 사용함</p>
<h4 id="tag">@Tag</h4>
<p>API 그룹 설정
테그에 설정된 name이 같은 것끼리 하나의 api그룹으로 묶음
controller나 Controller의 메서드 영역에 설정</p>
<h4 id="schema">@Schema</h4>
<p>모델에 대한 정보를 작성함
각 필드값들에 대한 설명이나 기본값, 허용가능한 값 등 api문서를 더 상세히 기술하는 것에 사용함.</p>
<h4 id="operation">@Operation</h4>
<p>API동작에 대한 명세를 작성하기 위해 사용
summary에는 간략한 설명, description에는 상세 설명을 기제한다.</p>
<h4 id="parameter">@Parameter</h4>
<p>파라미터에 대한 설명을 작성한다. name에 반드시 해당 파라미터의 이름을 적어야한다.</p>
<h4 id="postmapping">@PostMapping</h4>
<p>API POST요청을 처리한다.
뒤에 있는 주소로 POST를 하여 받은 데이터를 저장한다.</p>
<h4 id="requsetbody">@RequsetBody</h4>
<p>API POST,PUT,PATCH와 같이 사용한다. 요청 본문에 있는 데이터를 전송한다.</p>
<h4 id="data">@Data</h4>
<p>아래의 클래스에 있는 getter,setter을 자동으로 생성해준다.</p>
<h4 id="apiresponses-apiresponse">@ApiResponses, @ApiResponse</h4>
<p>api 코드에 대한 반환 값을 입력한다.</p>
<h4 id="securityrequirements">@SecurityRequirements</h4>
<p>API엔드포인트에서 보안 요구사항을 정의
아무것도 없다면 보안 요구사항을 무효화하거나 변경하는 것</p>
<h4 id="getmapping">@GetMapping</h4>
<p>http GET요청을 처리한다
뒤에 있는 주소로 GET을 해서 받아온다</p>
<h4 id="noargsconstructor">@NoArgsConstructor:</h4>
<p>기본 생성자를 자동으로 생성.</p>
<h4 id="allargsconstructor">@AllArgsConstructor:</h4>
<p>모든 필드를 초기화하는 생성자를 자동으로 생성.</p>
<h4 id="sequencegenerator">@SequenceGenerator</h4>
<p>예시
@SequenceGenerator(name=&quot;EMP_SEQ&quot;, allocationSize=25)
속성</p>
<ul>
<li>name
필수 속성이며 @GeneratedValue의 설정된 name을 설정하면 해당 @GeneratedValue가 붙은 컬럼에 적용된다.</li>
<li>allocationSize
선택사항이며 시퀀스 번호가 증가할 때 증가할 양을 설정한다.</li>
<li>catalog</li>
<li>선택사항이며 시퀀스의 카탈로그 이다.</li>
<li>initialValue
선택사항이며 시퀀스의 시작값을 설정한다.</li>
<li>schema
선택사항이며 시퀀스의 스키마이다.</li>
<li>sequenceName
데이터베이스에 저장될 시퀀스 개체의 이름을 설정한다.</li>
</ul>
<h4 id="id-generatedvalue">@Id/ @GeneratedValue</h4>
<p>@Id는 PK를 형성해주는역할을 함
@ GeneratedValue를 같이 사용해서 4가지 방법으로 자동생성하는 방법이 있음</p>
<ol>
<li><p>@GeneratedValue(strategy = GenerationType.IDENTITY)
기본키 생성을 데이터베이스에게 위임하는 방식
데이터베이스가 자동으로 생성하게 된다.
JPA가 처리할때 데이터베이스에 먼저 insert쿼리를 실행 해서 PK를 먼저 받아오는 방식</p>
</li>
<li><p>@GeneratedValue(strategy = GenerationType.SEQUNCE)
DB의 Sequence Object를 이용해서 DB가 생성한다
@SequenceGenerator가 필요함
@GeneratedValue(strategy = GenerationType.SEQUENCE,</p>
<pre><code>                 generator=&quot;USER_PK_GENERATOR&quot;)</code></pre><p>이렇게 사용하는데, generator에는 SquenceGenerator의 이름이 들어가야 한다.
DB에서 받아와서 생성하고 allocationSize로 증가 범위를 조절한다.</p>
</li>
<li><p>@GeneratedValue(strategy = GenerationType.TABLE)
테이블을 이용해서 생성한다.
방식은 Seq방식과 동일하지만 최적화되지 않은 것을 사용하기 때문에 성능에 이슈가 있다</p>
</li>
<li><p>@GeneratedValue(strategy = GenerationType.AUTO)
기본 설정 값으로 자동 생성한다.</p>
</li>
</ol>
<h4 id="builder">@Builder</h4>
<p>변수를 .변수명(값)으로 지정할 수 있게 해주는 패턴이다. 유지보수가 용이하다.</p>
<h4 id="autowired">@Autowired</h4>
<p>생성자,수정자의 의존성을 자동으로 주입한다.</p>
<h4 id="param">@Param</h4>
<p>메서드 매개변수를 명시적으로 연결해주는 역할을 한다.</p>
<h4 id="column">@Column</h4>
<p>name : 필드와 매핑할 테이블의 컬럼 이름
nullable : null값의 허용 여부 설정. 기본값은 true</p>
<h4 id="table">@Table</h4>
<p>엔티티와 매핑할 테이블을 지정
name : 매핑할 테이블 이름
schema : DB에서 schema를 매핑</p>
<h4 id="schema-1">@Schema</h4>
<p>description : 클래스나 필드의 대한 설명 추가
example : 문서에서 보여줄 예제 값을 설정
minimum(필드) : 최소값 제한
maximum(필드) : 최대값 제한</p>
<h4 id="enumerated">@Enumerated</h4>
<p>EnumType.ORDINAL, EnumType.STRING
enum의 값이나 이름을 데이터베이스에 저장한다.</p>
<h4 id="transient">@Transient</h4>
<p>엔티티 내부에서 DB와 매핑되지 않도록 한다.</p>
<h4 id="component">@Component</h4>
<p>Autowired가 가능하도록 SpringBean에 등록한다.
String으로 빈의 이름을 설정할 수 있음</p>
<h4 id="requestbody">@RequestBody</h4>
<p>HTTP요청을 자바 객체로 변환하여 객체에 저장한다.</p>
<h4 id="requiredargsconstructor">@RequiredArgsConstructor</h4>
<p>의존성 주입 방법중에 생성자 주입을 임의의 코드없이 자동으로 설정해 줌</p>
<h4 id="slf4j">@Slf4j</h4>
<p>추가 로깅 인터페이스로 디버깅 상세화 제공</p>
<h4 id="requestparam">@RequestParam</h4>
<p>Http에서 해당 이름으로 바인딩 할 수 있게 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA 테스트케이스 (3)]]></title>
            <link>https://velog.io/@swnote_02/JAVA-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A4-3</link>
            <guid>https://velog.io/@swnote_02/JAVA-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A4-3</guid>
            <pubDate>Sat, 18 Jan 2025 06:45:17 GMT</pubDate>
            <description><![CDATA[<h2 id="📌--문제">📌  문제</h2>
<p>테스트 케이스를 작성 중에 시스템 단위 테스트를 하는데, 단위 테스트 범위가 Mapper에 들어가 있어서 정확히 시간테스트를 진행할 수 없었다. Mapper은 Xml파일에서 얼마나 빠른 속도로 SQL을 통해 DB에서 데이터를 끌어올 수 있는지를 체크해야하는데, 정확한 시간을 잡아낼 수 없었다.</p>
<p>원래는 @Autowired를 통해 의존성 주입을 하고 사용했는데, 전체 시간 속에서 다른 mapper을 불러오려면 AOP를 활용해서 불러와야 했다.</p>
<hr>
<h2 id="📌aop">📌AOP?</h2>
<p>Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다.
어떤 로직을 기준으로 핵심적인 관점, 북적인 관점으로 나누었느냐, 그리고 그 관점으로 모듈화 한다는 것이다.</p>
<pre><code class="language-java">@Aspect
@Component
public class testAspect</code></pre>
<p>이런 식으로 클래스에 @Aspect를 이용해서 Aspect클래스라는 것을 명시한다. 그리고 @Component를 붙여
스프링 빈으로 등록한다.</p>
<pre><code class="language-java">@Around(&quot;excution(경로))
public Object test(ProceedingJoinPoint joinPoint) throws Throwable{
        long start = System.nanoTime();
        Object result = joinPoint.proceed();
        long end = System.nanoTime();
 }</code></pre>
<p> 이런 식으로 @Aspect를 이용해서 타겟 메서드를 감싸서 특정 Advice를 실행하겠다는 것이다. excution에 붙은 경로는 패키지 경로가 들어간느데, 이는 특정 객체 아래에 모든 메서드에 Aspect를 적용한다는 것이다.</p>
<hr>
<h2 id="📌테스트에-적용">📌테스트에 적용</h2>
<p> 테스트에 적용하는 것은 간단하다. 기본적으로 Junit 테스트를 진행하는 것과 동일하게 작동하기 때문에
 차이점은 없고 이번에 할 테스트는 전체 시간중에서 얼마나 그 메서드가 동작하는지 확인하는 것이기 때문에 전체 시간을 재고 그 시간과 AOP로 호출한 Service계층의 속도를 비교할 것이다.</p>
<pre><code class="language-java">    @Autowired
    private SelectAOP selectAop; // AOP 클래스

    @Autowired
    private SelectController controller;

    @Test
    void getExecutionTime(){
        long start = System.nanoTime();
        controller.getExecution();
        long end = System.nanoTime();

        long methodExecutionTime = selectAop.getMethodExecutionTime();
</code></pre>
<p>이런 식으로 적용해서 각각의 시간을 잴 수 있다.
이렇게 AOP를 호출해서 하면 좋은 점이 안에 있는 메서드를 따로 테스트에 호출하지 않아도
geExecution이 실행되면서 발생하는 DB커넥션 테스트를 진행 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA 테스트케이스 (2)]]></title>
            <link>https://velog.io/@swnote_02/JAVA-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A4-1</link>
            <guid>https://velog.io/@swnote_02/JAVA-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A4-1</guid>
            <pubDate>Thu, 09 Jan 2025 11:43:32 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-문제">📌 문제</h2>
<p>@Valid 를 사용해서 처리하고 있었다. 이제까지는 핸들러로 Exception을 잡아서 해결하고 있었는데,
여러모로 불편한 점이 많아서 그렇게 하지 않고 사용자 정의 어노테이션으로 해결하려고 한다.</p>
<hr>
<h3 id="방법-1-aop">방법 1. AOP</h3>
<p>AOP(Aspect-Oriented Programming)방법을 쓰면 어노테이션 방법을 쓰면서 throw exception을 유연하게 처리할 수 있다.</p>
<pre><code class="language-java">@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface MyNotNull {
    String message() default &quot;Field must not be null&quot;;
    String apiMessage() default &quot;INVALID_INPUT&quot;;
}</code></pre>
<p>이런 식으로 어노테이션을 만들고</p>
<pre><code class="language-java">import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyNotNullAspect {

    @Before(&quot;@annotation(myNotNull) &amp;&amp; args(value,..)&quot;)
    public void validateNotNull(MyNotNull myNotNull, String value) {
        if (value == null || value.trim().isEmpty()) {
            String defaultMessage = myNotNull.message();
            ApiCodeEnum apiCode = ApiCodeEnum.valueOf(myNotNull.apiMessage());
            throw new CommonException(apiCode, defaultMessage);
        }
    }
}</code></pre>
<p>이렇게 어노테이션 검사식을 추가할 수 있다.
같은 동작을 하긴 하는데, 이전과 다른 점은 Jakara에서 제공하는 @Valid어노테이션을 사용하면 
ConstraintViolationException을 발생시킨다. 하지 않는 방법이 있지만 이건 나중에 다루도록 하고,
AOP방식을 사용하면 내가 원하는 Exception을 터트릴 수 있다는 점에서 관리가 유연하다는 점이 있다.</p>
<pre><code class="language-java">public void processRequest(@MyNotNull(message = &quot;Name is required&quot;, 
apiMessage = &quot;INVALID_NAME&quot;) String name) {
    // 메서드 실행 전에 AOP로 유효성 검사 수행
}</code></pre>
<p>이런 식으로 사용한다.</p>
<hr>
<h3 id="방법-2-커스텀-validator클래스-사용">방법 2. 커스텀 Validator클래스 사용</h3>
<p>이전 방법에서는 어노테이션만 만들었다면 이번에는 Jakrta처럼 Validator클래스를 만들어서
그에 따른 동작을 하게하는 방법이다.</p>
<pre><code class="language-java">public interface Validator&lt;T&gt; {
    void validate(T value) throws CommonException;
}</code></pre>
<p>이렇게 범용 인터페이스를 정의하고</p>
<pre><code class="language-java">public class MyNotNullValidator implements Validator&lt;String&gt; {
    private final String defaultMessage;
    private final ApiCodeEnum apiCode;

    public MyNotNullValidator(String defaultMessage, ApiCodeEnum apiCode) {
        this.defaultMessage = defaultMessage;
        this.apiCode = apiCode;
    }

    @Override
    public void validate(String value) {
        if (value == null || value.trim().isEmpty()) {
            throw new CommonException(apiCode, defaultMessage);
        }
    }
}</code></pre>
<p>이렇게 사용하면, 그에따른 동작을 하게 만들어진다.
이 방법의 장점은 유연성과 재사용성을 모두 챙겼다는 점이다.
그리고 만약에 나중에 다른 어노테이션을 사용하려고 했을 때도 독립정의된 인터페이스로 작동하기 때문에
독립적으로 작동한다는 장점이 있다.</p>
<pre><code class="language-java">Validator&lt;String&gt; validator = new MyNotNullValidator(&quot;Value cannot be null&quot;, ApiCodeEnum.INVALID_INPUT);
validator.validate(inputValue);</code></pre>
<p>이렇게 사용한다.</p>
<hr>
<h3 id="방법-3-jakarta어노테이션">방법 3. Jakarta어노테이션</h3>
<p>지금까지 사용했던 방법들이 좋은 방법들이지만 지금 하려는 것에 치명적인 문제점이 있다.
위 방법대로 사용하면 @Vlidate로 검사를 시작할 수 없다.
모두 독립적인 방법으로 검사를 진행하기 때문에 기존에 있던 코드를 모두 변경해줘야 하는 참사가
발생한다.
그래서 호완이 가능한 Jakarta 어노테이션을 사용한다.</p>
<pre><code class="language-java">import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = MyNotNullValidator.class) // 검증기를 연결
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) // 적용 대상
@Retention(RetentionPolicy.RUNTIME) // 런타임에 유지
public @interface MyNotNull {

    // 기본 에러 메시지
    String message() default &quot;This field cannot be null or empty&quot;;

    // API 메시지 커스텀
    String APImessage() default &quot;INVALID_FIELD&quot;;

    // 그룹 지정 (기본값: {})
    Class&lt;?&gt;[] groups() default {};

    // 확장에 사용되는 Payload (기본값: {})
    Class&lt;? extends Payload&gt;[] payload() default {};
}
</code></pre>
<p>이렇게 어노테이션을 만들 수 있다.</p>
<pre><code class="language-java">public class MyNotNullValidator implements ConstraintValidator&lt;MyNotNull, String&gt; {

    private String defaultMessage;
    private ApiCodeEnum apiMessage;

    @Override
    public void initialize(MyNotNull constraintAnnotation) {
        this.defaultMessage = constraintAnnotation.message();  // 기본 메시지
        this.apiMessage = constraintAnnotation.APImessage();   // API 메시지
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.trim().isEmpty()) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(MessageUtils.getMessage(&quot;error.&quot; + apiMessage,defaultMessage))
                    .addConstraintViolation();
            return false;
        }
        return true;
    }
}</code></pre>
<p>이렇게 컨트롤러를 만들어줘야 하는데 하나하나 살펴보면</p>
<pre><code class="language-java">@MyNotNull(message=&quot;not&quot;,apiMessage=ApiCodeEnum.Error_code)</code></pre>
<p>이렇게 어노테이션을 사용하게 될 것이다.
그럼 not이라는 메세지를 담은 API_Error_code가 ConstraintViolationException으로 나오게 되는 것이다.</p>
<hr>
<h2 id="📌해결">📌해결</h2>
<p>기존 코드를 모두 바꿀 수 없으니 방법 3을 사용하기로 하였다.
근데 문제가 아직 하나 남아있다. 우리는 기존에 터트려야하는 Exception이 CommonException이다. 
하지만 3번 방법을 사용하게 되면 원하는 에러를 발생시킬 수 없다.</p>
<p>그래서 방법을 찾아봤는데 총 두가지의 방법이 있었다.</p>
<h3 id="방법-1-isvalid안에서-exception발생">방법 1. isValid안에서 Exception발생</h3>
<p>생각해보면 매우 간단한 방법인데</p>
<pre><code class="language-java">@Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.trim().isEmpty()) {
               throw new CommonException(apimessage,defualtmessage);
        }
        return true;
    }</code></pre>
<p>이렇게 하면 검사를 함과 동시에 CommonException을 발생시킬 수 있다. 
가장 큰 장점은 쉽다는 것이다. 이렇게 하면 별다른 핸들러가 필요없이 하나의 코드에서 처리할 수 있다.</p>
<p>하지만 단점이 있다. 바로 Springboot 프레임워크의 규칙을 위반한다는 것이다.</p>
<p>이게 무슨말이냐, 프레임워크는 정해진 틀이있고 그 안에서 발생할 수 있는 예외가 정해져있다.
하지만 CommonException은 정의한 Exception이고 이것은 프레임워크에서 발생하면 안되는 오류이다.
따라서 프레임워크는 아래와 같은 메세지를 보낸다.</p>
<pre><code>&quot;An unexpected error occurred&quot;</code></pre><p>사실 오류 처리만 제대로 하고 있다면 무시해도 별 상관은 없다.(라고 공식문서에 나와있다)
하지만 프레임워크의 특성상 웬만하면 구조는 건들지 않는 것이 좋다.
나중에 어떤 문제가 터질지 모르는 것이니.</p>
<hr>
<h3 id="방법-2-exceptionhandler이용">방법 2. ExceptionHandler이용</h3>
<p>CommonException을 처리할 수 없다면 예외를 catch해서 다른 예외로 바꾸는 핸들러를 사용하면 된다.</p>
<pre><code class="language-java">@RestControllerAdvice
@Validated
public class GlobalExceptionHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException ex) {
        // 예외 메시지를 추출
        List&lt;String&gt; errorMessages = ex.getConstraintViolations().stream()
            .map(violation -&gt; violation.getPropertyPath() + &quot;: &quot; + violation.getMessage())
            .collect(Collectors.toList());

        // CommonException으로 변환하여 던짐
        throw new CommonException(ApiCodeEnum.ERROR_938, errorMessages);
    }
}</code></pre>
<p>천천히 살펴보자</p>
<pre><code class="language-java">@RestControllerAdvice
@Validated</code></pre>
<p>RestControllerAdvice는 사실 여기서는 필요없는 어노테이션이다.
원래 해주는 역할은 @Valid가 Controller단에 걸려있다면 그걸 잡는 역할을 하는데,
여기서는 service단에 걸어놨기 때문에 필요없는 어노테이션이다.</p>
<p>@Validated는 @Valid를 처리할 것이라는 명시적 어노테이션인것 같다.(확실하지 않다..)</p>
<pre><code class="language-java">@ExceptionHandler(ConstraintViolationException.class)</code></pre>
<p>아래 나오는 메서드에서 ConstraintViolationException을 처리하겠다는 명시이다.
그냥 메서드로 작성할 수 있지만 다른 오류를 같이 처리하기 위해 묶어서 작성한다.</p>
<pre><code class="language-java">handleConstraintViolationException(ConstraintViolationException ex) {
        // 예외 메시지를 추출
        List&lt;String&gt; errorMessages = ex.getConstraintViolations().stream()
            .map(violation -&gt; violation.getPropertyPath() + &quot;: &quot; + violation.getMessage())
            .collect(Collectors.toList());</code></pre>
<p>ConstraintViolationException에서 발생한 예외 메세지를 담아서 리스트화 한다.
리스트로 만드는 이유는 아래 CommonException에서 받는 형식이기 때문이다.</p>
<pre><code class="language-java">throw new CommonException(ApiCodeEnum.ERROR_938, errorMessages);</code></pre>
<p>CommonException을 터트린다.</p>
<p>이런 방식으로 하니까 확실히 코드가 나눠져서 보기에는 편할 수 있었다.
하지만 이전 포스트에서 설명 했다시피 @Valid는 발생시킬 수 있는 예외가 총 3가지가 있다.
보통 컨트롤러 단에서 잡으니 2개는 잡을 필요가 없지만
그래도 핸들러를 만드려면 다 만들어 줘야 사용하기에 용이하다.
이런 점에서 약간 불편하지만 이런 방식을 채택하는게 유지보수와 사용용이성면에서 좋은 결과를 가질
 수 있는것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA 테스트케이스 (1)]]></title>
            <link>https://velog.io/@swnote_02/JAVA-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A41</link>
            <guid>https://velog.io/@swnote_02/JAVA-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A41</guid>
            <pubDate>Mon, 06 Jan 2025 14:20:56 GMT</pubDate>
            <description><![CDATA[<p>순서가 좀 꼬이긴 했는데 당일 발견하고 해결한 문제를 다뤄야 할 것 같아서 
순서가 다소 꼬이더라도 하루마다 작성하려고 한다.</p>
<hr>
<h2 id="📌-세팅">📌 세팅</h2>
<pre><code class="language-java">public class SignupVO {
    @Valid
    private Account account;
    // 기타 필드와 메서드
}</code></pre>
<p>이런 식으로 생긴 JAVA코드를 @Valid를 이용해서 유효검증을 하려고 한다.</p>
<pre><code class="language-java">@NotNull(message =&quot;null password&quot;)
    private String pw;</code></pre>
<p>@NotNull 어노테이션을 사용해서 Null 필드를 사용하지 않겠다고 하였고</p>
<pre><code class="language-java">@Override
    public Account signUp(@Valid SignupVO signupVO, String mobile)</code></pre>
<p>이런 식으로 메서드를 재정의 해서 @Valid를 적용하여 SignupVO를 검증하게 하였다.</p>
<hr>
<h2 id="📌-문제">📌 문제</h2>
<h3 id="1-valid가-적용되지-않는다">1. @Valid가 적용되지 않는다.</h3>
<p>분명 @Valid를 적용하면 유효성 검증을 할 수 있다고 하였다. 하지만 이 코드의 경우 그냥 넣었을 때는 유효성 검증이 불가능 하였다.
MethodArgumentNotValidException 이라는 예외가 등장하여야 하는데
다른 예외가 터지는것으로 보아 검증이 제대로 이뤄지지 않는 것이었다.</p>
<h3 id="1-valid가-적용되지-않는다-해결">1. @Valid가 적용되지 않는다 (해결)</h3>
<p>문제의 해결은 간단했다.</p>
<pre><code class="language-java">@Validate
public class TestCase{

    public void case1(@Valid signUpVO signupvo){
    ...
    }

}</code></pre>
<p>이런 식으로 작성해야 했다. public class위에 Validate를 적용해줘야 아래 @Valid가 정상적으로 검증을 수행하였다.</p>
<hr>
<h3 id="2-jakartavalidationconstraintdeclarationexception-hv000151-오류-발생">2. jakarta.validation.ConstraintDeclarationException: HV000151 오류 발생</h3>
<pre><code class="language-java">public class AuthServiceImpl implements AuthService {
    @Override
    public Account signUp(@Valid SignupVO signupVO, String mobile) {
        // 메서드 구현
    }
}</code></pre>
<p>이런 식으로 작성된 코드를 실행하였더니 오류가 발생하였다.
찾아보니 호완성 오류라는데 해결은 간단했다.</p>
<h3 id="2-jakartavalidationconstraintdeclarationexception-hv000151-오류-발생해결">2. jakarta.validation.ConstraintDeclarationException: HV000151 오류 발생(해결)</h3>
<pre><code class="language-java">public interface AuthService {
    Account signUp(SignupVO signupVO, String mobile);
}</code></pre>
<p>문제의 인터페이스이다. AuthService를 인터페이스로 작성하였는데, 그 안에있는 메서드인 signUp이 호완이 되지 않아 발생하는 문제이다.</p>
<pre><code class="language-java">public interface AuthService {
    Account signUp(@Valid SignupVO signupVO, String mobile);
}</code></pre>
<p>이렇게 바꿔주면 정상적으로 작동하게 된다. @Valid또한 문법인지라, 인터페이스에 맞게 작성해줘야 한다.</p>
<hr>
<h3 id="3-jakartavalidationconstraintviolationexception을-commonexception으로-변환-불가">3. jakarta.validation.ConstraintViolationException을 CommonException으로 변환 불가</h3>
<p>테스트케이스의 핵심은 CommonException으로 다국어화 한 Exception을 발생시키는 것이었다.
DB에서 그것들을 불러와서 Exception Enum으로 정의된 것을 통해 획일화된 Exception을 발생시켜야 하는데, 이것을 하기 위해서 GlobalExceptionHandler을 사용하였다.</p>
<pre><code class="language-java">public class GlobalExceptionHandler {

    // ConstraintViolationException을 처리
    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException ex) {
        // 오류 메시지 추출
        List&lt;String&gt; errorMessages = ex.getConstraintViolations().stream()
            ...
        // CommonException 던지기
        throw new CommonException(ApiCodeEnum.ERROR_938, errorMessages);
    }</code></pre>
<p>이렇게 작성해서 ConstraintViolationException을 잡으려고 하였다. 하지만 </p>
<pre><code>org.opentest4j.AssertionFailedError: Unexpected exception type thrown, 
Expected :class com.duegosystem.core.config.exception.CommonException
Actual   :class jakarta.validation.ConstraintViolationException</code></pre><p>이런 오류문구(테스트결과)를 얻게 되었다.
해석하면, 예외를 기대한 것은 commonException이었는데 ConstraintCiolationException이 터졌다는 말이었다.</p>
<p>즉, 핸들러가 작동하지 않아서 원래 오류인 ConstraintCiolationException이 터졌다는 말이다.</p>
<hr>
<h3 id="3-jakartavalidationconstraintviolationexception을-commonexception으로-변환-불가해결">3. jakarta.validation.ConstraintViolationException을 CommonException으로 변환 불가(해결)</h3>
<p>많은 서칭과 GPT를 통해 해결책을 얻었다.</p>
<pre><code class="language-java">@RestControllerAdvice
@Validated
public class GlobalExceptionHandler {

    // ConstraintViolationException을 처리
    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException ex) {
        // 오류 메시지 추출
        List&lt;String&gt; errorMessages = ex.getConstraintViolations().stream()
            ...

        // CommonException 던지기
        throw new CommonException(ApiCodeEnum.ERROR_938, errorMessages);
    }</code></pre>
<p>먼저 @RestContollerAdvice를 추가하였다. 여기서의 역할은 아래의 public  class를 전역적인 class로 만들어서 어디서 예외가 발생하던 잡을 수 있도록 만드는 것이라는데, 이건 Controller 계층에서 해당하는 말이고, 지금 작업하는 계층은 Service계층이기 때문에 별로 의미있는 어노테이션은 아니다.</p>
<p>문제점은 테스트에 있었다. SpringBoot와 Junit으로 테스트 케이스를 작성하였는데, 두개의 특성상
GlobalExceptionHandler에 예외를 던지기 전에 예외를 반환하고 테스트 케이스를 종료한다.</p>
<p>그랬기 때문에 아무리 메인 코드를 바꿔도 처리가 되지 않았던 것. 이걸 하기 위해 방법이 두가지가 있었다.</p>
<ol>
<li>Mock를 사용하는 것(실패)
예전에도 한번 시도해 봤었다.<pre><code class="language-java">@Autowired
private MockMvc mockMvc;</code></pre>
이런 식으로 MockMvc로 가상의 객체를 만들어서 API를 전달하는 방법인데, 어째선지 이유를 찾지는 못했는데 NPE(NullPointException)이 발생해서 빈번히 실패했었다.
이유는 Mock객체가 만들어지지 않았다고 하는데...
이건 나중에 다시 찾아보는 것으로</li>
<li>Intellij Http 사용(성공)
Intellij에는 http라는 기능이 있다. 처음 써봤을 때 매우 신기했는데,
API를 직접 만들어서 서버단에서 전송할 수 있는 기능이다.<pre><code class="language-http">GET https://localhost:8080/auth/service</code></pre>
이런 방법으로 .http파일에 작성하면 옆에 실행 버튼이 뜨게 된다.
그 버튼을 누르면 해당 주소에서 API를 불러오게 된다.</li>
</ol>
<pre><code>POST https://localhost:8080/auth/service
Content-Type: application/json

{
  &quot;groupName&quot;: &quot;그룹명&quot;,
  &quot;members&quot;: [
    &quot;회원1&quot;,
    &quot;회원2&quot;,
    &quot;회원3&quot;
  ],
  &quot;date&quot;: {
    &quot;year&quot;: 2018,
    &quot;month&quot;: 1,
    &quot;day&quot;: 24
  }
}</code></pre><p>이런 식으로 POST도 할 수 있다. 이렇게 하면 내가 원하는 데이터를 API형식으로 웹에 전달하게 된다.
물론 서버는 켜져 있어야 정상적으로 작동한다.</p>
<p>아무튼 이런 방식으로 내가 원하는 데이터를 만들어서 전송시켰더니
CommonException이 정상적으로 나오는 것을 볼 수 있었다.</p>
<hr>
<h2 id="📌-추가적으로-생각해-본-것">📌 추가적으로 생각해 본 것</h2>
<h3 id="1-valid의-exception은-methodargumentnotvalidexception을-발생시켜야-하는데-왜-constraintviolationexception이-터질까">1. @Valid의 Exception은 MethodArgumentNotValidException을 발생시켜야 하는데 왜 ConstraintViolationException이 터질까?</h3>
<p>이유는 @Valid의 사용 위치에 따라 달랐다</p>
<pre><code class="language-java">@RestController
public class AuthController {

    @PostMapping(&quot;/signUp&quot;)
    public ResponseEntity&lt;String&gt; signUp(@Valid @RequestBody SignupVO signupVO) {
        return ResponseEntity.ok(&quot;Signup successful&quot;);
    }
}</code></pre>
<p>이렇게 Controller메서드에서 @Valid가 사용될 때는 
MethodArgumentNotValidException을 발생시킨다.</p>
<pre><code class="language-java">@Service
@Validated
public class AuthService {

    public void signUp(@Valid SignupVO signupVO) {
        // 메서드 로직
    }
}</code></pre>
<p>그리고 이렇게 Service메서드에서 @Valid를 호출하면
ConstraintViolationException가 발생하게 된다.</p>
<pre><code class="language-java">@Controller
public class FormController {

    @PostMapping(&quot;/submitForm&quot;)
    public String submitForm(@Valid FormData formData, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return &quot;error&quot;;
        }
        return &quot;success&quot;;
    }
}</code></pre>
<p>또 이런 경우가 있다는데 한번도 보진 못했다 Controller메서드에서 발생하는 것 같다.
BindException을 발생시킨다. form에 매핑이 제대로 되지 않으면 발생하는 것 같다.</p>
<p>이런 3가지 경우로 발생시킬 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JAVA 테스트케이스 (0)]]></title>
            <link>https://velog.io/@swnote_02/Junit%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-TestCase%EC%9E%91%EC%84%B11</link>
            <guid>https://velog.io/@swnote_02/Junit%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-TestCase%EC%9E%91%EC%84%B11</guid>
            <pubDate>Sun, 29 Dec 2024 07:19:47 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-작성-이유">📌 작성 이유</h2>
<p>회사에서 테스트 케이스를 작성하게 되었는데 막히는 일도 있고 
정리하면 좋을 것 같아서 시작하게 되었다.</p>
<hr>
<h2 id="📌-사용한-언어-및-프레임워크">📌 사용한 언어 및 프레임워크</h2>
<ul>
<li>JAVA Junit</li>
</ul>
<hr>
<h2 id="테스트-케이스의-종류와-방법">테스트 케이스의 종류와 방법</h2>
<p>먼저 테스트 케이스를 작성하려면 테스트케이스에 어떤 종류가 있고
무슨 방식으로 테스트 케이스를 작성하는지 알아야 한다고 생각한다</p>
<h4 id="블렉박스-테스트black-box-test">블렉박스 테스트(Black-Box Test)</h4>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/d33d94d4-e078-4aee-b133-dc060de0cb6b/image.png" alt="">
위 그림처럼 내부 경로에 대한 지식을 보지 않고 프로그램의 외부 규격서에 의거해서
입력 데이터와 출력 데이터가 규격서에 정해진 바와 같은 결과를 얻을 수 있는지 테스트한다.
요구사항이 명확하고 얻어야하는 결과가 명확할 때 사용할 수 있다.</p>
<h4 id="화이트박스-테스트white-box-test">화이트박스 테스트(White-Box Test)</h4>
<p><img src="https://velog.velcdn.com/images/swnote_02/post/6ad8b4ec-20df-4572-aef0-3fa2c135f625/image.png" alt="">
위 이미지 처럼 프로그램의 코드나 모듈에 접근하여 요구에 맞게 잘 동작하는가에 초점을 둔 테스트이다. 프로그램 내부 구조의 타당성 여부를 시험하고, 내부구조를 테스트해서 프로그램 루틴에 대해 시험한다.</p>
<h4 id="단위-테스트unit-test">단위 테스트(Unit Test)</h4>
<p>모듈테스트(Module Test)라고도 한다.
테스트 가능한 가장 작은 단위를 개별적으로 테스트 한다</p>
<h4 id="통합-테스트intergration-test">통합 테스트(Intergration Test)</h4>
<p>프로그램 또는 그 구성 요소인 모듈 등의 정보 시스템 하나하나의 구성 요소를 결합해서 
인터페이스와 각 결합 단계에서의 기능을 확인한다. 컴포넌트나 서브시스템 단위이다.
이외에도 많은 테스트가 있다</p>
<hr>
<p>여기서 할 부분은 화이트 박스 테스트를 진행하고
코드에 대한 단위 테스트를 진행하게 되었다.
그럼 화이트박스 테스트라고 했으니 코드는 이미 적용되어 있고
단위 테스트를 진행해야 하는데 단위테스트를 진행하는 방법이 뭘까?</p>
<hr>
<h2 id="테스트-과정">테스트 과정</h2>
<ol>
<li>원시 코드를 통해 애플리케이션의 구조를 이해</li>
<li>검증 기준(커버리지) 선정</li>
<li>각 경로를 구동시키는 테스트 데이터 준비</li>
</ol>
<p>크게 이렇게 이뤄진다.
애플리케이션의 구조를 이해하기 위해 논리흐름도를 작성할 수 있으나,
너무 코드가 길기 때문에 논리코드가 지저분하게 나와서
굳이 작성하지 않고 각 기능을 하는 메서드별로 테스트케이스를 작성하기로 하였다.</p>
<h3 id="검증-기준커버리지-선정">검증 기준(커버리지) 선정</h3>
<h4 id="문장-커버리지statement-coverage">문장 커버리지(Statement Coverage)</h4>
<p>프로그램 내의 모든 명령문을 적어도 한번 실행해야 한다
조건문에 관계 없이 구문 실행 개수로 계산한다.</p>
<h4 id="결정선택-분기-커버리지branch-coverage">결정(선택), 분기 커버리지(Branch Coverage)</h4>
<p>각 분기의 결정 포인트 내의 조건식이 적어도 한번은 참과 거짓의 결과를 수행해야 한다.
(문장 커버리지를 포함한다)</p>
<h4 id="조건-커버리지condition-coverage">조건 커버리지(Condition Coverage)</h4>
<p>각 분기의 결정 포인트 내의 각 개별 조건식이 적어도 한번은 참과 거짓이 수행되어야 함.</p>
<h4 id="조건결정-커버리지conditiondecicion-coverage">조건/결정 커버리지(Condition/Decicion Coverage)</h4>
<p>전체 조건 뿐 아니라 개별 조건식도 참과 거짓의 결과가 한번씩 나와야 한다.</p>
<p>이게 말만 들었을 때는 분기 커버리지와 조건 커버리지의 차이가 잘 느껴지지 않는다.
그래서 아래 코드를 보면</p>
<pre><code class="language-java">if(a&gt;0 || b&lt;0){
System.out.println(&quot;ok&quot;);
}</code></pre>
<p>이 코드를 사용한다고 하였을 때</p>
<table>
<thead>
<tr>
<th></th>
<th>a</th>
<th>b</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td>test1</td>
<td>0</td>
<td>0</td>
<td>false</td>
</tr>
<tr>
<td>test2</td>
<td>0</td>
<td>-1</td>
<td>true</td>
</tr>
<tr>
<td>test3</td>
<td>1</td>
<td>0</td>
<td>true</td>
</tr>
<tr>
<td>이런 진리표를 얻을 수 있다.</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>이 표에 분기커버리지를 적용하면</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>test2과 test3 둘중 하나와 test1이면 100%를 얻을 수 있다.
분기 커버리지는 if분기에서 조건문이 참/거짓을 만족하면 되기 때문이다.</p>
<p>하지만 조건 커버리지를 적용하면
test2,3을 적용하면 100%를 얻을 수 있다.
조건 커버리지는 분기와 다르게 결과값에 상관없이 조건문 안의 조건들이 각각 true와 false를 가져야 한다.</p>
<p>그래서 곧 조건!=분기가 적용되게 된다.
조건커버리지를 만족하지만 분기 커버리지는 만족하지 않을 수 있는 것이다.</p>
<p>이런 부분을 개선하기위해 나온 것이 조건/결정 커버리지이다. 
test1,2,3을 모두 사용해야 100%를 얻을 수 있다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>지금까지 테스트 케이스 작성을 위한 준비 과정이었다.
코드를 미리 볼 수 있는 만큼 테스트 데이터를 잘 준비해서 모든 커버리지를 만족할 수 있는 케이스를
작성해야한다.</p>
]]></description>
        </item>
    </channel>
</rss>