시작하며

현재 사용하고 있는방법을 기록해두고 더 좋은 코드를 찾을시 비교하기위한 기록용이다.

 

ex)

 @RequestPart(required = false, value = "img_file") MultipartFile imgFile
            , @Valid @RequestPart(value = "data") InsertRequest insertRequest
            , BindingResult bindingResult

1차. 입력값의 유무

if (insertRequest == null){
            throw  new RootException(ApiStatusCode.BAD_REQUEST, "잘못된 정보입니다.");
        }

2차. 입력값의 이상유무

       BidingResult를 사용하여 최초의 Request값에 대한 검증을 한다.

 

Request객체의 검증 어노테이션을 활용한다.

@NotNull, @NotBlank, @NotEmpty, @Size, @Min, @Max 등 

 if (bindingResult.hasErrors()) {
            log.info("BindingResult hasErrors");
            throw new RootException(ApiStatusCode.BAD_REQUEST,  "잘못된 정보입니다.");
        }

 

3차. Service 계층 세부 내용검증

 if (insertRequest.getStartDt().compareTo(insertRequest.getEndDt()) >= 1) {
            //0이면같음, 음수면 정상
            throw new RootException(ApiStatusCode.BAD_REQUEST, "노출 시작 일자가 종료 일자 보다 늦을 수 없습니다.");
        }

 

시작하며

Spring을 사용하며 @RequestParam, @RequestBody, @ModelAttribute 3가지 어노테이션을 활용하였다.

대충 어떻게 사용하는지는 알지만 한번 정리의 필요성을 느꼈다. 정리 고고

 

@RequestParam

public String getTest(@RequestParam("name") String name){
	System.out.println(name);
}

@RequestParam은 1개의 HTTP 요청 파라미터를 받기위해서 사용한다.

 

- value

- defaultValue

- required = true(기본설정 true / )false 설정가능

 

required = false 설정을 하지않고 파라미터를 전송하지않을시 400 에러가 발생한다.

@RequestBody

public String getTest(@RequestBody Test test){
	System.out.println(test);
}

Json형태의 HTTP Body내용을 Java 객체로 변환시켜주는 역할을 한다.

Get과 Post 방식 모두 사용은 가능하지만, @RequestBody는 Body안에 Json을 포함해야한다.

하지만 Get은 QueryParameter방식으로 보내기때문에 Get은 Get답게 Post는 Post답게 사용해야한다.

 

 

@ModelAttribute 

@Getter
@Setter
public class TestDto {
	private String name; 
	private int age; 
} 

public class TestController { 

	@RequestGetMapping(value = "/hi") 
	public void getTest(@ModelAttribute("test") TestDto test){ 
		System.out.println("이름 : " + test.getName());
		System.out.println("나이 : " + test.getAge()); 
	} 
}

@ModelAttribute는 Form형태 기반의 요청들을 받는다. Get Post 둘다 사용가능하다. Get의 경우는 Query Parameter로 요청데이터를 보내고, Post의 경우 x-www-from-urlencoded형태로 요청데이터를 보낸다.단, DTO에 바인딩시 Setter를 통해 바인딩된다. Setter가 없으면 바인딩되지않음!

myBatis 프로젝트를 사용하며 배운점들을 정리한다.

 

Dynamic Query는 상황에 다라 분기 처리를 통해 동적인 SQL문을 작성할 수 있다.

 

myBatis는 XML에서 쿼리를 작성하기 때문에 별도의 표기법이 필요하다.

정적쿼리

  <select id="findByFaq" resultType="FaqTotalEntity">
        SELECT  A.FAQ_ID
                , A.IS_VIEW
                , A.FAQ_TITLE
                , A.FAQ_CONTENT
                , A.START_DT
                , A.END_DT
                , B.IMAGE_ID
                , B.NAME
                , B.PATH
        FROM TB_FAQ A
            LEFT JOIN TB_FAQ_IMAGE B
            ON A.FAQ_ID = B.FAQ_ID
        WHERE A.FAQ_ID = #{faqId}
    </select>

if, where를 활용한 동적쿼리

<select id="findByTopList" resultType="FaqTotalEntity">
         SELECT  A.FAQ_ID
              , A.VIEW_COUNT
              , A.IS_VIEW
              , A.ADMIN_ID
              , A.FAQ_TITLE
              , A.IS_TOP
              , A.TOP_ORDER
              , A.START_DT
              , A.END_DT
              , B.PATH
         FROM TB_FAQ A
            LEFT JOIN TB_FAQ_IMAGE B
            ON A.FAQ_ID = B.FAQ_ID
        WHERE A.IS_TOP = 'Y'
        <if test='isView == "N" or isView == "Y"'>
            AND A.IS_VIEW = #{isView}
        </if>
        <if test="faqTitle != null">
            AND A.FAQ_TITLE LIKE CONCAT('%',#{faqTitle},'%')
        </if>
        <if test="startDt != null">
            AND A.START_DT <![CDATA[>=]]> #{startDt}
        </if>
        <if test="endDt != null">
            AND A.END_DT <![CDATA[<=]]> #{endDt}
        </if>
        ORDER BY
        <choose>
            <when test="order != null and orderType == 'ASC'">
                ${order} ASC
            </when>
            <when test="order != null and orderType == 'DESC'">
                ${order} DESC
            </when>
            <when test="order == null and orderType == null">
                A.TOP_ORDER
            </when>
        </choose>
    </select>

그외 choose - when - otherwise , trim, where, set 등 다양한 조건식이있다.

#{ }, ${ }의 차이점

위 동적쿼리문에서 orderby의 ${order}를 #{order}로 기존에는 사용하였다.

PostMan으로 API Request를 호출하였다.

그런데 쿼리문이 order by 정렬이 되지않은 상태로 출력되었다.

즉, DB의 결과값고 PostMan의 결과값이 달랐다.

 

Log상 myBatis의 쿼리문도 정상적으로 날라가는게 보였는데 원인을 찾지 못해 삽질을 시작했다.

 

그결과

#{ }는 파라미터 값이 ' '로 감싸져들어가고, ${ }는 파라미터 값이 그대로 들어간다.

${ }로 변경하니 정상 출력되었다.

 

 

참고사이트 : myBatis 공식홈페이지

getGeneratedKeys

프로젝트시 myBatis를 사용하여, API 요청시 2개의 테이블을 Insert 해야한다.

 

  • EX) Order라는 A 테이블과 OrderStatus라는 두개의 테이블이 존재한다.

A라는 테이블은 OrderID라는 PK값을 가지고, B라는 테이블은 OrderStatusID라는 PK를 가진다.

여기서 A테이블의 OrderID PK를 B테이블의 FK로 연결하지는 않았다.

※ OrderID의 PK이므로 중복이 존재하지않기에 JOIN 및 서브쿼리를 사용

이떄 하나의 API요청으로 두테이블은 모두 Insert하려한다.

 

API 요청시 Order 테이블의 Insert로 OrderID의 PK(auto_increment)가 생성이될때,

해당 PK값을 반환하여 OrderStatus 테이블을 Insert하려한다.

 

mapper.xml 설정

useGeneratedKeys="true" keyProperty="Entity의 PK 필드명"

 

myBatis 공식문서 의 내용으로 "생성키에 대한 JDBC 지원을 허용. 지원하는 드라이버가 필요하다. true 로 설정하면 생성키를 강제로 생성한다. 일부 드라이버(예를들면, Derby)에서는 이 설정을 무시한다." 라고 명시되어있다.

 

해당 내용을 해석해보면 Insert 후 PK값을 반환하는것이 아닌, PK값을 키를 먼저 반환한다고 생각이된다.(개인의 생각)

 

Insert 부분에는 당연히 PK는  자동생성이기에 입력되지않는다.

해당 설정을 사용하면 Entity에 PK값이 반환되어 .get시 PK값이 출력이된다.

 

참으로 신기하다.

 

참고 사이트 : myBatis 공식문서

 

 

 

시작하며

@Transactional JPA를 활용한 프로젝트를 개발하였지만, 깊이 있는 이해도는 없었다. 명확하게 설명하지 못한다면,

제대로 알고있는게 아니라 생각한다. 공부내용 정리글이다.

 

Transactional 정의

데이터베이스에서 트랜잭션은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다.

여기서 단위는 더이상 쪼개질 수 없는 최소의 연산이다.

Transactional 특징

  • 원자성(Atomicity): 트랜잭션의 모든 작업이 완전히 성공하거나 완전히 실패하는 단일 단위로 처리되도록 보장하는 능력이다. 중간 단계까지 실행되고 실패하는 일이 없도록 하는 것이다.
  • 일관성(Consistency): 각 데이터 트랜잭션이 데이터베이스를 일관성 있는 상태에서 일관성 있는 상태로 이동해야 함을 의미한다. 즉, 트랜잭션이 성공적으로 완료하면 언제나 동일한 데이터베이스 상태로 유지하는 것을 의미한다.
  • 격리성(Isolation): 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. 즉, 데이터베이스는 스트레스 테스트를 통과해야 한다. 과부하로 인해 잘못된 데이터베이스 트랜잭션이 발생하지 않아야 한다.
  • 지속성(Durability): 성공적으로 수행된 트랜잭션은 영원히 반영(기록)되어야 함을 의미한다. 트랜잭션은 로그에 모든 것이 저장된 후에만 commit 상태로 간주 될 수 있다. 데이터베이스내의 데이터는 트랜잭션의 결과로만 변경되어야 하며, 외부 영향에 의해 변경 될 수 없어야 한다.
  • 데이터베이스 트랜잭션 처리에 있어서 중요한 요소이며, 데이터베이스가 제공하는 일관성과 안정성을 보장합니다.

REST 구성

  • 자원(Resource) - URI
  • 행위(Verb) - HTTP Method
  • 표현(Representations)

REST 특징

  1. 자원(Resource) - URI를 통해 자원을 명시하고, HTTP 메소드를 사용하여 해당 자원에 대한 CRUD 작업을 수행합니다.
  2. 메시지(Message) - RESTful API에서는 HTTP 프로토콜의 메시지 포맷을 이용하여 요청과 응답을 처리합니다. 예를 들어, JSON, XML 등의 포맷을 사용할 수 있습니다.
  3. Stateless(무상태성) - RESTful API는 서버 측에서 상태 정보를 유지하지 않고, 요청에 대한 응답만 전달합니다. 이러한 특징을 가지기 때문에, 서버의 확장성과 성능을 높일 수 있습니다.
  4. 캐시(Cache) - RESTful API에서는 HTTP 프로토콜의 캐시 기능을 이용하여 캐시를 적극적으로 사용할 수 있습니다. 이를 통해, 클라이언트와 서버 간의 통신을 최적화할 수 있습니다.
  5. 인터페이스(Interface) - RESTful API는 클라이언트와 서버 간의 인터페이스를 일관성 있게 유지하도록 설계됩니다. 이를 통해, 클라이언트와 서버 간의 상호 작용을 단순화하고, 상호 운용성(interoperability)을 향상시킬 수 있습니다.
  6. 계층형 구조(Layered System) - RESTful API에서는 서버와 클라이언트 사이에 다양한 계층을 둘 수 있습니다. 이를 통해, 시스템의 확장성과 보안성을 향상시킬 수 있습니다.
  7. 자체 표현(Self-descriptive) - RESTful API는 메시지 자체에 대한 정보를 포함하고 있기 때문에, 메시지를 이해할 수 있는 자체 표현 형식을 사용합니다. 이를 통해, 클라이언트와 서버 간의 상호 작용을 간소화하고, 확장성을 향상시킬 수 있습니다.

RESTful API 디자인 가이드

1. URI 마지막에 슬래시(/)를 포함하지 않는다.

 - URI 경로 마지막은 반드시 문자여야 한다. 슬래시(/) 다음에 의미 있는 값을 추가하지 않으면 혼동이 올 수 있다.

X : http://api.test.com/users/
O : http://api.test.com/users

2. 슬래시(/)를 사용하여 계층적 관계를 나타낸다.

 - 슬래시 문자는 리소스 간의 계층적 관계를 나타내기 위해 URI 경로에 사용된다.

ex) http://api.test.com/users/1/posts

3. URI 가독성을 높이려면 하이픈(-)을 사용해야한다.

 - URI를 사람들이 쉽게 스캔하고 해석 할 수 있도록 하이픈(-) 문자를 사용하여 가독성을 높인다. 영어로 공백이나 하이픈을 사용하면 URI에서는 모두 하이픈을 사용해야 한다.

X : http://api.test.com/users/posts_comments
O : http://api.test.com/users/posts-comments

4. URI는 소문자를 사용해야한다.

X : http://api.test.com/users/postsComments
O : http://api.test.com/users/posts-comments

5. 행위(Method)는 URI에 포함하지 않는다.

X : http://api.test.com/users/1/delete-post/1
O : http://api.test.com/users/1/posts/1

6. 파일 확장자는 URI에 포함하지 않는다.

X : http://api.test.com/users/1/posts/1/photo.jpg
O : http://api.test.com/users/1/posts/1/photo

 

HTTP 응답 상태코드

상태코드

200 클라이언트의 요청을 정상적으로 수행함
201 클라이언트가 어떠한 리소스 생성을 요청, 해당 리소스가 성공적으로 생성됨
(POST를 통한 리소스 생성 작업 시)

상태코드

400 클라이언트의 요청이 부적절 할 경우 사용하는 응답 코드
401 클라이언트가 인증되지 않은 상태에서 보호된 리소스를 요청했을 때 사용하는 응답 코드
  (로그인 하지 않은 유저가 로그인 했을 때, 요청 가능한 리소스를 요청했을 때)
403 유저 인증상태와 관계 없이 응답하고 싶지 않은 리소스를 클라이언트가 요청했을 때 사용하는 응답 코드
  (403 보다는 400이나 404를 사용할 것을 권고. 403 자체가 리소스가 존재한다는 뜻이기 때문에)
405 클라이언트가 요청한 리소스에서는 사용 불가능한 Method를 이용했을 경우 사용하는 응답 코드

상태코드

301 클라이언트가 요청한 리소스에 대한 URI가 변경 되었을 때 사용하는 응답 코드
  (응답 시 Location header에 변경된 URI를 적어 줘야 합니다.)
500 서버에 문제가 있을 경우 사용하는 응답 코드

Servlet => Spring => Spring Boot 순서로 공부를하며 DTO와 Entity의 차이점은 간략하게 파악하였다.

하지만 DTO와 VO는 어떠한 차이점이 있을까?

여러 참고 사이트를 비교하며 정리해본다.

DTO(Data Transfer Object)

DTO는 데이터를 전달하기 위한 객체이다. 계층간의 Getter / Setter를 이용하여 데이터를 주고 받는다.

여러 레이어에서 사용할 수 있지만, 주로 View와 Controller사이에서 사용한다.

DTO는 Getter / Setter 메소드를 포함하고, 그 외의 비즈니스 로직은 포함하지 않는다.

EX)

public class MemberDto {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Setter가 아닌 생성자를 이용해서 초기화하는 경우 불변 객체로 활용 할 수 있다. 불변 객체로 만들시 데이터를 전달하는 과정에서 데이터가 변조되지 않는다.

public class MemberDto {
    private final String name;
    private final int age;

    public MemberDto(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

VO(Value Object)

VO는 값 자체를 표현하는 객체이다. VO는 객체들의 주소가 달라도 값이 같으면 동일한것으로 여긴다. 

 EX) 고유번호가 다른 만원 2장이 있다. 이 둘은 고유번호(주소)는 다르지만 값(10,000원)은 동일하다.

VO는 Getter 메소드와 함께 비즈니스 로직도 포함 할 수 있다. 단, Setter 메서드는 포함하지 않고, 값의 비교를 위해

equals()와 hashCode() 메소드를 오버라이딩 해줘야한다.

public class Money {
    private final String currency;
    private final int value;

    public Money(String currency, int value) {
        this.currency = currency;
        this.value = value;
    }

    public String getCurrency() {
        return currency;
    }

    public int getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return value == money.value && Objects.equals(currency, money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(currency, value);
    }
}

// MoneyTest.java
public class MoneyTest {
    @DisplayName("VO 동등비교를 한다.")
    @Test
    void isSameObjects() {
        Money money1 = new Money("원", 10000);
        Money money2 = new Money("원", 10000);

        assertThat(money1).isEqualTo(money2);
        assertThat(money1).hasSameHashCodeAs(money2);
    }
}

EX) equals()와 hashCode() 메서드를 오버라이딩 하지 않았을 때

EX) equals()와 hashCode() 메서드를 오버라이딩 하였을 때

Entity

Entity는 실제 DB 테이블과 매핑되는 핵심 클래스이다. 이를 기준으로 테이블이 생성되고 스키마가 변경된다.

따라서, 절대로 Entity를 요청이나 응답값을 전달하는 클래스로 사용해서는 안된다.

그리고 비즈니스 로직을 포함할 수 있다.

public class Member {
    private final Long id;
    private final String email;
    private final String password;
    private final Integer age;

    public Member() {
    }

    public Member(Long id, String email, String password, Integer age) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.age = age;
    }
}

세 객체 비교

분류 DTO VO Entity
정의 레이어간 데이터 전송용 객체 값 표현용 객체 DB 테이블 매핑용 객체
상태 변경 여부 Setter 존재시 가변,
Setter 비 존재시 불변
불변 Setter 존재시 가변,
Setter 비 존재시 불변
로직 포함 여부 로직을 포함 할 수 없다. 로직을 포함 할 수 있다. 로직을 포함 할 수 있다.

 

참고 사이트 : 인비의 DTO vs VO

Cache-Control

  • Cache-Control : no-cache, no-store, must-revalidate
  • Pragma : no-cache(HTTP 1.0 하위 호환)

캐시 지시어

  • Cache-Control : no-cache
    • 데이터는 개시해도 되지만, 항상 원래 서버에 검증하고 사용
  • Cache-Control : no-store
    • 데이터에 민감한 정보가 있으므로 저장하면 안됨(메모리에서 사용하고 최대한 빨리 삭제)
  • Cache-Control : must-revalidate
    • 캐시 만료 후 최초 조회시 원래 서버에 검증해야함.
    • 원래 서버 접근 실패시 반드시 오류가 발생해야함 - 504(Gateway Timeout)
    • must- =revalidate는 캐시 유효 시간이라면 캐시를 사용함
  • Pragma : no-cache
    • HTTP 1.0 하위 호환

검증 헤더와 조건부 요청

검증헤더

  • 캐시 데이터와 서버데이터가 같은지 검증하는 데이터
  • Last-Modified, ETag

조건부 요청 헤더

  • 검증 헤더로 조건에 따른 분기
  • If-Modified-Since : Last-Modified 사용
  • If-None-Match : ETag 사용
  • 조건이 만족하면 200 OK
  • 조건이 만족하지 않으면 304 Not Modified

예시

  • 데이터 미변경시
    1. 캐시 : 2023년 4월 13일 15:00:00 / 서버 : 2023년 4월 13일 15:00:00
    2. 304 Not Modified => 헤더 데이터만 전송(Body 미포함)
    3. 전송 용량 헤더만!
  • 데이터 변경시
    1. 캐시 : 2023년 4월 13일 15:00:00 / 서버2023년 4월 13일 16:00:00
    2. 200 OK => 모든 데이터 전송(Body 포함)
    3. 전송 용량 헤더/바디!

검증 헤더와 조건부 요청의 단점

  • 1초 미만 단위로 캐시 조정이 불가능하다.
  • 날짜 기반의 로직을 사용한다.
  • 데이터를 수정해서 날짜가 다르지만, 같은 데이터를 수정해서 데이터 결과가 같은경우
    • 예) A => B => A
  • 서버에서 별도의 캐시 로직을 관리하고 싶은경우
    • 예) 스페이스나 주석처럼 크게 영향이 없는 변경에서 캐시를 유지하고 싶은 경우

ETag

  • 캐시용 데이터에 임의의 고유한 버전 이름을 달아둔다.
  • 예) ETag : "v1.0",ETag : "1234567890"
  • 데이터가 변경되면 이름을 변경함.
  • 예) ETag : "aaaaa" => "bbbbb"
  • 같으면 유지!  다르면 다시받기!

강의출처 : https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC/dashboard

캐시 미적용

캐시가 없을경우 1.1M의 JPG를 다운로드받는다.

두번째 요청시에도 동일하게 다운로드 받는다.

  • 데이터가 변경되지 않아도 계속 네트워크를 통해서 데이터를 다운로드 받아야한다.
  • 인터넷 네트워크는 매우 느리고 비싸다.
  • 브라우저 로딩 속도가 느리다.

캐시 적용

첫번째 요청시 응답결과를 캐시에 저장한다.

두번째 요청시 캐시에서 조회가가능하다.

  • 캐시 덕분에 캐시 가능 시간동안 네트워크를 사용하지 않아도 된다.
  • 비싼 네티워크 사용량을 줄일 수 있다.
  • 브라우저 로딩 속도가 매우 빠르다.

캐시 시간초과

  • 캐시 유효 시간이 초과하면, 서버를 통해 데이터를 다시 조회하고, 캐시를 갱신한다.
  • 캐시 유효 시간이 초과해서 서버에 다시 요청하면 두가지 상황이 나타난다.
    1. 서버에서 기존데이터를 변경함.
    2. 서버에서 기존데이터를 변경하지 않음.
  • 캐시 만료후에도 서버에서 데이터를 변경하지 않음.
  • 데이터를 전송하는 대신에 저장해 두었던 캐시를 재사용 할 수 있다.
  • 클라이언트의 데이터와 서버의 데이터가 같다는 사실을 확인할 수 있는 방법 필요

  • HTTP 헤더에 Last-Modified 시간 설정

캐시 만료 후 조회시 웹 브라우저 요청시 데이터 최종수정일을 서버에 전송한다.

서버와 캐시의 데이터 수정일이 동일할 경우

  • HTTP응답시 304 NoT Modified 헤더 메타정보만 응답
  • HTTP Body 없음
  • 캐시를 재사용한다.

 

강의출처 : https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC/dashboard

+ Recent posts