기록은 기억의 연장선

더 많은 것을 기억하기 위해 기록합니다


  • Home

  • Tags

  • Categories

  • Archives

  • Search

[aws] iam

Posted on 2019-02-08 | Edited on 2020-11-02 | In aws | Comments:

http://victorydntmd.tistory.com/67

AWS 리소스에 대해 접근 권한을 부여하는 것이다.
개인에게 직접 부여할 수도 있고, 그룹에 권한을 부여한 뒤 사용자를 그룹에 포함시킬수도 있다.

유저를 생성하면 access key와 secret key가 저장된 csv 파일을 다운받을 수 있는데, 이는 외부에서 AWS 리소스 접근시에 사용되므로 잘 저장해두어야 한다.

Read more »

[aws] presigned url

Posted on 2019-02-08 | Edited on 2020-11-02 | In aws | Comments:

서버에서 직접 multipart를 받아 S3 버킷에 업로드하면 서버쪽에서 multipart 파일을 쥐고 있어야 하는 둥 리소스 낭비가 크므로 S3에 접근할 수 있는 Presigned url을 생성하여 클라이언트가 직접 업로드하게 한다.

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/dev/PresignedUrlUploadObjectJavaSDK.html
presigned url 이란 AWS S3 버킷에 바로 파일을 업로드 할 수 있는 URL을 말한다.

생성 및 사용

java sdk는 maven repository에서 간단하게 검색 가능하다
https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk

위 라이브러리를 임포트 하고 아래와 같이 작성하면 된다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Autowired // spring 사용 시 DI 가능하다!
private final AmazonS3 s3Client;

@Value("${aws.s3.bucket}")
private String bucketName;

public String getPresignedUrl(){
String fileName = UUID.randomUUID().toString();

Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 60; // 1시간
expiration.setTime(expTimeMillis);

GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectKey)
.withMethod(HttpMethod.PUT)
.withExpiration(expiration);

generatePresignedUrlRequest.addRequestParameter(Headers.S3_CANNED_ACL,
CannedAccessControlList.PublicRead.toString());

URL url = s3Client.generatePresignedUrl(generatePresignedUrlRequest);

return url.toExternalForm();
}

이렇게 생성된 presigned url에 PUT 요청으로 multipart data를 보내면 파일을 S3에 업로드할 수 있게 된다.
업로드는 expiration time 동안 유효하다.

참고로 위의 소스에는 접근하는 S3에 대한 인증정보가 없는데, 이는 AmazonS3에서 사용하는 ProfileCredentialProvider 때문이다.
이 클래스는 저장된 credential을 읽어서 인증하는데, 실행되는 환경에 따라 이 credential 을 찾는 우선순위가 있다.
(ec2에서 실행되면 ec2에 지정된 credential, pc에 저장된 credential 등등)

업로드한 파일 접근

업로드 이후 presigned-url에서 query param을 제거하고 GET 요청하면 해당 파일을 보거나(이미지) 다운로드 받을 수 있는데, 그냥 접근하면 access denied가 발생한다.
그러므로 presigned url 생성할 때 public read 권한을 줘야하고, 아래와 같이 하면 된다.
http://zstd.github.io/amazon-presigned-url/

1
2
3
4
5
6
7
8
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectKey)
.withMethod(HttpMethod.PUT)
.withExpiration(expiration);

// 이부분
generatePresignedUrlRequest.addRequestParameter(
Headers.S3_CANNED_ACL,
CannedAccessControlList.PublicRead.toString());
Read more »

[db] mysql port 파라미터 안될 때

Posted on 2019-02-08 | Edited on 2020-11-02 | In db | Comments:

docker로 3307 포트로 mysql을 띄웠는데 포트 변경해서 접속하는 옵션인 -P 를 아무리 줘도 무시되는 상황이 발생했다.

찾아보니 -h 가 localhost일 경우 소켓을 사용해서 포트 옵션이 무시되므로, 127.0.0.1을 써줘야 한다고 한다
https://serverfault.com/questions/306421/why-does-the-mysql-command-line-tool-ignore-the-port-parameter/306423?newreg=1c57059c005942bfbf0735b86bc570d6

1
mysql -u root -h 127.0.0.1 -P 3307
Read more »

[jpa] 웹 어플리케이션과 영속성 관리

Posted on 2019-02-07 | Edited on 2020-11-02 | In jpa | Comments:

트랜잭션 범위의 영속성 컨텍스트

스프링 컨테이너의 기본 전략

스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다.
이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고, 트랜잭션이 끝날 때 영속성 컨텍스트를 종료하는 방법이다.

트랜잭션 범위의 영속성 컨텍스트

스프링 트랜잭션 AOP는 @Transactional 어노테이션이 붙은 메서드가 호출될 때 트랜잭션을 시작한다.
메서드가 성공적으로 수행되면 해당 트랜잭션을 커밋하고, 예외가 발생한다면 트랜잭션을 롤백한다.
이 시점에 영속성 컨텍스트에 추가적인 작업을 호출한다.

  • 트랜잭션을 커밋하면(메서드가 성공적으로 수행되면) 영속성 컨텍스트를 플러시해서 변경내용을 반영한 후 데이터베이스 트랜잭션을 커밋한다.
  • 예외가 발생하면 플러시를 호출하지 않고 데이터베이스 트랜잭션을 롤백한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Controller
class HelloController{
@Autowired
private HelloService helloService;

@PostMapping("/{teamId}/members")
public String addMember(@PathVariable Integer teamId, @RequestBody MemberDTO memberDTO){
Member member = helloService.logic(teamId, memberDTO); // 준영속 상태
// ...

return "/member/add_result";
}
}

@Service
class HelloService{
@Autowired
private MemberRepository memberRepository;

@Transactional
public void logic(Integer teamId, MemberDTO memberDTO){
Member member = memberRepository.addMember(memberDTO.toEntity());
Team team = teamRepository.findTeam(teamId);
team.setMemberCnt(team.getMemberCnt()+1);

return member;
}
}

@Repository
class MemberRepository{
@PersistenceContext
EntityManager em;

public Member addMember(Member member){
return em.persist(member);
}
}

@Repository
class TeamRepository{
@PersistenceContext
EntityManager em;

public Team findTeam(Integer id){
return em.find(Team.class, id);
}
}
  • logic 메서드가 실행될 때 트랜잭션이 시작된다.
  • logic 메서드가 종료되면 member, team에 대한 변경 내용이 데이터베이스에 플러시되고 트랜잭션이 커밋된다.
  • 예외가 발생하면 변경 내용이 데이터베이스에 플러시되지 않고, 시작한 트랜잭션은 롤백된다.

트랜잭션과 영속성 컨텍스트의 생명주기가 같으므로, 트랜잭션이 끝남과 동시에 영속성 컨텍스트도 종료된다.
즉, HelloController에서 logic의 결과로 받은 Member 엔티티는 준영속 상태이다.

참고로 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.
위의 상황에서 MemberRepository와 TeamRepository는 서로 다른 엔티티 매니저를 주입받았지만 같은 영속성 컨텍스트를 사용한다.
이와 반대로 같은 엔티티 매니저를 사용해도 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다(잘 안그려진다 상황이…)

준영속 상태와 지연로딩

위에서 언급했듯이 트랜잭션과 영속성 컨텍스트의 생명주기가 같기 때문에, 트랜잭션이 끝난 뒤의 엔티티는 준영속 상태가 된다.
즉 위의 상황에서 logic 메서드가 끝남과 동시에 영속성 컨텍스트도 종료되었기 때문에 결과로 반환된 Member 엔티티는 준영속 상태가 되는 것이다.
그리고 당연하게도, 준영속 상태인 엔티티에 지연로딩을 수행하게 되면 오류가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller
class HelloController{
@Autowired
private HelloService helloService;

@PostMapping("/{teamId}/members")
public String addMember(
@PathVariable Integer teamId,
@RequestBody MemberDTO memberDTO,
ModelMap modelMap){

Member member = helloService.logic(teamId, memberDTO);
// ...

modelMap.add("member", member);
modelMap.add("teamName", member.getTeam().getName()); // lazy loading! but an exception occured

return "/member/add_result";
}
}

위처럼 지연로딩을 하게 되면 하이버네이트 기준으로 org.hibernate.LazyInitializationException이 발생한다.
이는 영속성 컨텍스트에 들어있지 않은 준영속 상태의 엔티티에 지연로딩을 시도했기 때문에 발생하는 것이다.
트랜잭션이 끝나면서 영속성 컨텍스트도 같이 종료되었기 때문에 반환된 엔티티는 자연스럽게 준영속 상태가 되었고, 이런 현상이 발생한 것이다.

하지만 생각해보면, 영속성 컨텍스트가 트랜잭션과 동시에 종료되고 프레젠테이션 계층까지 전파되지 않는것은 좋은 선택(?)이다.
만약 영속성 컨텍스트를 프레젠테이션 계층까지 열어두었다면 프레젠테이션 계층에서도 변경 감지가 동작하게 되어 위험하고,
각 계층이 가지는 역할자체도 모호해지기 때문이다.

하지만 위에서 봤다시피, 지연로딩이 동작하지 않는다는 점은 꽤나 골치아픈 일이다.
결국 위의 상황을 해결하고 싶으면 아래의 2가지 방법을 사용해야 한다.

뷰에 필요한 엔티티를 미리 로딩해두는 방법

말 그대로 영속성 컨텍스트가 살아있을 때 뷰에 필요한 엔티티들을 미리 다 로딩하거나 초기화해서 반환하는 방법이다.
아래의 3가지 방법이 있다.

글로벌 페치 전략 수정

fetchType을 EAGER로 바꾸는 방법이다.

1
2
3
4
5
class Member{
@ManyToOne(fetch=FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
}

Member 조회 시 항상 Team을 같이 로딩해서 가지게 되므로, 준영속 상태가 되어도 지연로딩 문제가 발생하지 않는다.
이미 로딩해서 가지고 있기 때문이다.

하지만 이 방식은 아래와 같은 문제를 가진다.

  • 사용하지 않는 엔티티를 로딩한다
    뷰에서 Team이 필요하지 않은 경우도 있을것이다. 하지만 항상 Team을 같이 조회해야 한다.

  • N+1 문제가 발생한다

    1
    2
    String sql = "SELECT m FROM Member m";
    List<Member> members = em.createQuery(sql, Member.class).getResultList();

    실행되는 SQL은 아래와 같다.

    1
    2
    3
    4
    5
    6
    SELECT * FROM Member;
    SELECT * FROM Team WHERE id = ?;
    SELECT * FROM Team WHERE id = ?;
    SELECT * FROM Team WHERE id = ?;
    SELECT * FROM Team WHERE id = ?;
    ....

    JPQL을 실행할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL만 참고하여 충실히 SQL을 만들기 때문에 발생한 현상이다.
    위와 같이 처음 조회한 수만큼 다시 SQL을 사용해서 조회하는 것을 N+1 문제라고 한다.

JPQL FetchJoin

봤다시피 fetchType을 EAGER로 바꾸는건 너무 비효율적이다.
fetchType을 LAZY로 설정하고 필요할 때만 같이 조회해오도록 하는것이 좋을 것이고, JPQL에서는 FetchJoin이라는 기능으로 이를 지원한다.
사용법은 간단하다.

1
2
String sql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = em.createQuery(sql, Member.class).getResultList();

간단히 조인 명령어 마지막에 FETCH 만 넣어주면 된다.
이렇게 하면 해당 대상까지 조인으로 함꼐 조회해온 뒤 엔티티에 바인딩해준다.

이 방식이 현실적인 대안이긴 하지만, 화면에 맞춘 리파지토리 메서드가 증가할 수 있다는 단점이 있다.
즉, 아래와 같은 메서드들이 생길 수 있다는 것이다.

  • Member만 조회해오는 repository.findMember 메서드
  • Member와 연관된 Team 까지 조회해오는 repository.findMemberWithTeam 메서드

이런식으로 계속 메서드가 추가되다보면 레파지토리와 뷰 간의 논리적인 의존관계가 발생하게 된다.
이런 상황에서는 최적화를 조금 포기하고 논리적 의존관계를 최소화하는 방법을 선택하던지(findMember와 findmemberWithTeam 통합),
최적화를 선택하고 논리적 의존관계를 가지고 가던지… 선택해야 한다.

사실상 성능에 미치는 영향이 미비하므로 뷰와 레파지토리의 의존관계가 급격하게 증가하는 것보다는 최적화를 포기하는것이 조금 나은 것 같다.

강제로 초기화

영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다.

1
2
3
4
Member member = em.find(Member.class, 1);
member.getTeam().getName(); // 프록시는 실제 사용하는 시점에 초기화된다

return member;

강제로 초기화했으므로 준영속 상태에서도 사용할 수 있게된다.

하이버네이트를 사용한다면 initialize 메서드를 사용해요 프록시를 강제로 초기화할 수 있다.

1
org.hibernate.Hibernate.initialize(member.getTeam());

하지만 이것도 결국 생각해보면 프리젠테이션 계층이 은근슬쩍 서비스 계층을 침범하는 상황이다.
프리젠테이션 계층에 필요한 엔티티를 서비스 계층에서 초기화하고 있기 때문이다.

프리젠테이션 계층과 서비스 계층 사이에 FACADE 계층이라는 것을 둬서 이런 논리적 의존관계를 완전히 제거할 수 있다.
FACADE 계층이랄게 특별할 건 없고, 그냥 중간에 계층하나를 더 두고
서비스 계층에서 받은 엔티티를 프리젠테이션 계층에서 필요한 형태로 가공해서 내려다주는 역할을 하는 것이다.
지연로딩 때문에 영속성 컨텍스트가 필요하므로, 트랜잭션은 FACADE 계층부터 시작해야한다.

얼핏보면 이렇게 함으로써 논리적 의존관계가 완전히 제거된 듯 보이지만,
실용적인 관점에서 보면 결국 코드를 훨씬 많이 작성하게 되고, 단순히 서비스 계층 호출을 위임하는 코드가 생길 가능성이 많다.

OSIV

OSIV(Open Session In View)는 영속성 컨텍스트를 뷰 까지 열어준다는 뜻이다.
영속성 컨텍스트가 살아있으면 엔티티는 영속 상태가 유지되므로, 뷰에서도 지연로딩을 사용할 수 있다.
이 기능의 핵심은, 뷰에서도 지연 로딩이 가능하다 이다.

요청 당 트랜잭션(Transaction Per Request)

뷰까지 영속성 컨텍스트를 열어두기 위해 가장 간단한 방법을 사용한다.
요청이 들어오자마자 필터나 인터셉터에서 트랜잭션을 시작하고, 요청이 끝날때 트랜잭션도 끝내는 것이다(트랜잭션과 영속성 컨텍스트의 생명주기가 같기 때문)

이렇게 하면 뷰에서도 지연로딩이 가능하므로 엔티티를 미리 초기화할 필요가 없다. 당연히 FACADE 계층도 필요없어진다.
하지만 트랜잭션이 프레젠테이션 영역까지 열리므로 프레젠테이션 계층이 엔티티를 변경할 수 있는 심각한 문제가 생긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
class HelloController{
@Autowired
private HelloService helloService;

@GetMapping("/member/{memberId}")
public String addMember(@PathVariable Integer memberId, ModelMap modelMap){

Member member = helloService.logic(memberId);
member.setName("XXXX"); // 보안상의 이유로 XXXX로 세팅해서 내림

modelMap.add("member", member);

return "/member/list";
}
}

뷰를 렌터링 한 후 트랜잭션이 커밋될 것이고, 이때 변경 내역이 플러시되면서 회원의 이름이 XXXX로 변경되는 참사가 발생할 것이다.
프레젠테이션 계층은 데이터를 보여주는 계층이다. 이런 행위가 절대 허용되어서는 안된다.
이를 막기 위한 방법들은 아래와 같다.

  • 엔티티를 읽기 전용 인터페이스로 제공

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    interface MemberView{
    // 보여줄 애들의 getter만 선언
    public String getName();
    }

    @Entity
    class Member implements MemberView{
    // getter 오버라이드
    }

    class MemberService{
    public MemberView getMember(Integer id){ // memberView 반환
    return memberRepository.findById(id);
    }
    }
  • 엔티티 래핑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class MemberWrapper{
    private Member member;

    public MemberWrapper(Member member){
    this.member = member;
    }

    public String getName(){
    return member.getName();
    }
    }

    class MemberService{
    public MemberWrapper getMember(Integer id){ // memberView 반환
    return new MemberWrapper(memberRepository.findById(id));
    }
    }
  • DTO
    전통적인 방법으로, 단순히 데이터만 전달하는 객체인 DTO를 만들고 보여줄 엔티티의 값을 세팅해서 내려주는 방법이다.
    사실 이 방법은 OSIV의 장점을 못 살리는 방법이다. 강제로 초기화의 조금 다른 방법일 뿐이다.

이러한 문제점들로 인해 요청 당 트랜잭션은 거의 사용하지 않는다.
요즘에는 서비스 레벨까지만 트랜잭션을 내리는 방법을 사용하는데, 이게 스프링 프레임워크에서 제공하는 OSIV가 선택한 방식이다.

스프링 OSIV

아래는 spring-orm.jar에 들어있는 클래스들 중 하나이다. 필요한 위치에 따라 선택해서 사용하면 된다.

  • JPA OEIV(OSIV) 서블릿 필터

    org.springframework.org.jpa.support.OpenEntityManagerInViewFilter

  • JPA OEIV(OSIV) 스프링 인터셉터

    org.springframework.org.jpa.support.OpenEntityManagerInViewInterceptor

스프링 OSIV는 아래와 같이 OSIV를 사용하기는 하지만 트랜잭션은 비즈니스 계층에서만 사용한다.

스프링 OSIV

  1. 클라이언트 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 트랜잭션은 시작하지 않는다.
  2. 서비스 계층에서 트랜잭션을 시작할 때(@Transactional) 1번에서 생성한 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
  3. 서비스 게층이 끝나면 영속성 컨텍스트를 플러시하고 트랜잭션을 커밋한다. 그리고 트랜잭션을 끝내지만 영속성 컨텍스트는 종료하지 않는다.
  4. 프레젠테이션까지 영속성 컨텍스트가 유지되므로 지연로딩이 가능하다.
  5. 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시는 하지 않는다.

    참고로 여기서 강제로 플러시를 호출해도 예외(javax.persistence.TransactionRequiredException이 발생한다)

다시 간단하게 정리하면,

  • 프레젠테이션 계층에서 트랜잭션이 없으므로 엔티티 변경이 불가능하다
  • 하지만 영속성 컨텍스트는 열려있으므로 지연로딩이 가능하다

이런것이 가능한 이유는, 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되기 때문이다. 이를 Nontransaction reads라고 한다.
(트랜잭션 없이 엔티티를 변경하면 javax.persistence.TransactionRequireException이 발생한다)

상당히 많은 부분이 해결되었지만, 여기에도 여전히 주의사항이 존재한다.
바로 프레젠테이션 계층에서 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 계층을 만났을 경우이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Controller
class HelloController{
@Autowired
private HelloService helloService;

@GetMapping("/member/{memberId}")
public String addMember(@PathVariable Integer memberId, ModelMap modelMap){

Member member = helloService.logic(memberId);
member.setName("XXXX"); // 보안상의 이유로 XXXX로 세팅해서 내림

helloService.biz();

modelMap.add("member", member);

return "/member/list";
}
}

@Service
class HelloService{
@Transactional
public void biz(){
// ...
}
}

결과는 member의 이름이 XXXX로 바껴버린다.
기존의 트랜잭션 단위로 영속성 컨텍스트를 열던 것을 요청 단위로 영속성 컨텍스트를 여는 것으로 확장했기 때문에
하나의 요청에서 여러 트랜잭션이 있을 경우 영속성 컨텍스트를 공유하게 되고, 위와 같은 현상이 발생하는 것이다.

나는 OSIV를 이용해 엔티티를 프레젠테이션까지 내리는 행위는 딱히 좋지 않다고 생각한다.
위와 같은 문제점도 있고, 엔티티와 프레젠테이션 사이에 논리적인 의존관계가 생기는 것을 막을 수 없게 될 테니까…
전통적인 방법인 필요한 애들만 DTO로 만들어서 내려주는 방법이 가장 괜찮은 것 같긴한데,
이것또한 서비스 영역과 프레젠테이션 영역에 은근한 의존관계가 생긴다는 말을 반박할수는 없는 것 같다.

OSIV 주의사항

영속성 컨텍스트가 리퀘스트까지 열린다는 것은, 커넥션을 유지하는 시간이 늘어남을 의미한다
이는 트래픽이 많아지면 OSIV를 사용하면 영속성 컨텍스트를 유지하는 영역이 트랜잭션 이상으로(request까지) 넓어지고,
이는 커넥션을 맺는 시간이 길어짐을 의미한다
(게다가 하나의 요청내에서는 db작업 외에 다른 많은 작업들이 수행될 수 있으므로(네트워크 통신 등), 커넥션을 계속 열어두는 행위는 좋지않다)

request 까지 커넥션을 유지하게 됨. 데이터베이스에서 데이터를 가져와야 하기 때문이다

이는 성능상 이슈를 발생시킬 수 있으므로(커넥션 풀 개수 부족), OSIV를 끄고 service에서 DTO로 변환해서 내려주는 방식을 택하는 것이 좋다

위에서 반박할 수 없다고 해놓고…

Read more »

[jpa] MultipleBagException

Posted on 2019-01-31 | Edited on 2020-11-02 | In jpa | Comments:

JPQL은 기본적으로 1 -> N 방향으로 조인이 1번 까지밖에 안된다
2번 이상하게 되면 MultipleBagException 이 발생한다.

1
2
3
// item아래 재고단위, 색상 등이 1:N으로 있다고 가정
query.join(item.stockUnits, stockUnit).fetchJoin()
.join(item.colors, color).fetchJoin() // MultipleBagException 발생

카테시안 곱이 일어나기 때문이라는데…(너무 많은 로우가 생기므로)
@OneToMany 관계를 List가 아닌 Set으로 바꿔주면 위 에러가 발생하지 않는다
https://www.thoughts-on-java.org/hibernate-tips-how-to-avoid-hibernates-multiplebagfetchexception/

이유는 모르겠다 젠장…

Read more »

API documentation으로 좋은 redoc

Posted on 2019-01-31 | Edited on 2020-11-02 | In openAPI | Comments:

https://github.com/Rebilly/ReDoc

작성한 OAS를 보기좋은 document로 만들어준다.

https://github.com/Rebilly/ReDoc/blob/master/cli/README.md

1
redoc-cli bundle <spec file>

하면 생성된다

Read more »

OpenApi codegen

Posted on 2019-01-30 | Edited on 2020-11-02 | In openAPI | Comments:

openapi generator

https://github.com/OpenAPITools/openapi-generator

Open Api Spec을 통해 code를 generate 해주는 라이브러리
server stub, client stub 생성 가능

gradle plugin

https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-gradle-plugin

openapi generator를 gradle에서 사용하기 위한 플러그인

1
2
3
dependencies {
classpath "org.openapitools:openapi-generator-gradle-plugin:3.3.0"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apply plugin: 'org.openapi.generator'

// 위 repository README의 Configuration 참조
openApiGenerate {
generatorName = "kotlin"
inputSpec = "$rootDir/specs/petstore-v3.0.yaml".toString()
outputDir = "$buildDir/generated".toString()
apiPackage = "org.openapi.example.api"
invokerPackage = "org.openapi.example.invoker"
modelPackage = "org.openapi.example.model"
modelFilesConstrainedTo = [
"Error"
]
configOptions = [
dateLibrary: "java8"
]
}

Tip

generate 된 코드에 상속 적용하기

allOf를 사용하면 generate된 코드에 상속을 사용할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ChildDTO:
title: ChildDTO
description: description
allOf:
- $ref: '#/components/schemas/ParentDTO'
- type: object
required:
- name
- age
properties:
name:
type: string
description: 이름
example: ㅋㅋ
age:
type: integer

아래와 같이 generate 됨

1
2
3
4
class ChildDTO extends ParentDTO{
String name;
Integer age;
}
Read more »

OAS 3.0

Posted on 2019-01-28 | Edited on 2020-11-02 | In openAPI | Comments:

document 문서

git : https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md
swagger : https://swagger.io/docs/specification/basic-structure/

meta data, server 정보

1
2
3
4
5
6
7
8
9
10
openapi: 3.0.0
info:
title: Sample API
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
version: 0.1.9
servers:
- url: http://api.example.com/v1
description: Optional server description, e.g. Main (production) server
- url: http://staging-api.example.com
description: Optional server description, e.g. Internal staging server for testing

data type

기본 타입

https://swagger.io/docs/specification/data-models/data-types/

  • string, number, integer, boolean, array, object 사용가능
  • string 은 date와 file을 포함함
  • array는 items가 필수로 와야함. items 아래 type 혹은 $ref

api 정의

path 정의

paths 아래에 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
paths:
/users:
get:
operationId: getUsers
summary: get user
description: get user description
post:
operationId: createUser
summary: create user
description: create given user
/users/{id}:
get:
...
patch:
deprecated: true
  • path 아래 http method 별로 선언 가능
  • summary랑 description 차이가 뭐지?
  • operationId는 나중에 code-gen 에서 메서드명으로 사용됨
  • path templating 할 수 있고, 파라미터에서 받을 수 있음
  • deprecated 할 수 있음

parameter 정의

https://swagger.io/docs/specification/describing-parameters/
기본 형태는 아래와 같음

1
2
3
4
5
6
7
8
9
10
11
paths:
/users/{id}:
get:
parameters:
- in: path
name: id # 상단의 path명과 같아야함
required: true
schema:
type: integer
minimum: 1
description: The user ID
  • 파라미터 형태, 이름, 필수여부 등을 줄 수 있음. 추가적인 정보는 여기 확인
    • in에는 path, query, header, cookie 가 올 수 있음
    • path의 경우 url에 path templating과 이름을 동일하게 해줘야 한다는 점 빼고는 모두 동일하게 사용 가능
  • schema에서 전달받은 파라미터에 대해 정의함. 타입, 최소값 등을 줄 수 있음. 추가적인 정보는 여기 확인

request body

https://swagger.io/docs/specification/describing-request-body/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
paths:
/pets:
post:
summary: Add a new pet
requestBody:
description: Optional description in *Markdown*
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
/pets:
post:
summary: Add a new pet
requestBody:
description: Optional description in *Markdown*
required: true
content:
application/json:
schema:
type: object
properties:

object 정의

https://swagger.io/docs/specification/components/
components 아래에 정의하고, 재사용을 목적으로 한다.
$ref 속성을 이용하고 #으로 참조한다.

1
$ref: '#/components/schemas/Item'

#는 현 위치를 말한다.

schemas

일반 오브젝트(DTO 등)

parameters

https://swagger.io/docs/specification/describing-parameters/#common-for-various-paths

Common Parameters for Various Paths 부분 참조

responses

https://swagger.io/docs/specification/describing-responses/

status 별로 선언가능하며 description, object 등을 내려줄 수 있다

enum reuse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
paths:
/products:
get:
parameters:
- in: query
name: color
required: true
schema:
$ref: '#/components/schemas/Color'
responses:
'200':
description: OK

components:
schemas:
Color:
type: string
enum:
- black
- white
- red
- green
- blue
Read more »

[db] 마스터 데이터, 트랜잭션 데이터

Posted on 2019-01-25 | Edited on 2020-11-02 | In db | Comments:

https://www.intricity.com/data-warehousing/master-data-vs-transaction-data/

마스터 데이터 : 사람이나 기관이 관리하는 데이터
트랜잭션 데이터 : 마스터 데이터에 대한 이벤트 데이터 같은 것. 구매, 주문 등

Read more »

[jpa] best practice

Posted on 2019-01-25 | Edited on 2020-11-02 | In jpa | Comments:

Spring orm이 제공하는 것 LocalContainerEntityManagerFactory
테이블의 기본키 이름은 테이블이름을 prefix로 가져가는것이 좋다
엔티티의 경우 참조라는 값이 있지만 RDB의 경우는 없기 때문
게다가 테이블간 관계설정할 때 외래키에 테이블의 이름을 쓰는 관례가 많으므로 헷갈리지 않기 위해 기본키도 테이블 이름을 prefix로 가져주는 것이 좋다

@PersistenceContext :
스프링이나 J2EE를 사용하면 컨테이너가 직접 엔티티매니저를 관리하고 제공해준다.
이 어노테이션은 컨테이너가 관리하는 엔티티 매니저를 주입하는 어노테이션이다

@PersistenceUnit : EntityManagerFactory를 주입받고자 할 때

@Transactional은 RuntimeException만 롤백한다

  • 엔티티 설계에서 참조할 수 있는 부분. 편의메서드가 내가 작성한 것 만큼 빡빡하게 조건검사를 하지 않는다

public void setMember(Member member){
this.member = member;
member.getOrders().add(this);
}

public void addOrder(Order order){
orders.add(order);
order.setmember(this);
}
근데 책에서 설명할때는 bug를 방지하여 조건검사를 어느정도 넣어주는것을 봤는데, 이걸 제거해도 되는것인지?

회원 중복검사를 하는 로직은 service 메서드에서 private으로 구현했다
이미 등록된 회원일 경우 exception을 반환하는 식으로 처리했다
이러한 검증 로직이 있어도 멀티스레드 안정성을 보장하기 위해 UK를 걸어주는 것이 좋다

  • 값이 setter를 통해 무분별하게 사용되지 않도록 메서드로 제한한다
    public void addStock(int quantity) {
    this.stockQuantity += quantity;
    }

public void removeStock(int quantity) {
int restStock = this.stockQuantity - quantity;
if (restStock < 0) {
throw new NotEnoughStockException(“need more stock”);
}
this.stockQuantity = restStock;
}

public void cancel() {

if (delivery.getStatus() == DeliveryStatus.COMP) {
    throw new RuntimeException("이미 배송완료된 상품은 취소가 불가능합니다.");
}

this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : orderItems) {
    orderItem.cancel();
}

}

  • 관계를 한방에 맺어주는 편의메서드를 제공한다. 이로인해 객체간의 관계도 파악가능하다.
    public static Order createOrder(Member member, Delivery delivery, OrderItem… orderItems) {

    Order order = new Order();
    order.setMember(member);
    order.setDelivery(delivery);
    for (OrderItem orderItem : orderItems) {
    order.addOrderItem(orderItem);
    }
    order.setStatus(OrderStatus.ORDER);
    order.setOrderDate(new Date());
    return order;
    }

  • 추가적인 정보를 제공해주는 메서드를 만든다
    public int getTotalPrice() {
    int totalPrice = 0;
    for (OrderItem orderItem : orderItems) {
    totalPrice += orderItem.getTotalPrice();
    }
    return totalPrice;
    }

  • 다른 엔티티와 값을 동기화한다(재고 등)
    public static OrderItem createOrderItem(Item item, int orderPrice, int count) { // 주문생성시 재고를 깎고

    OrderItem orderItem = new OrderItem();
    orderItem.setItem(item);
    orderItem.setOrderPrice(orderPrice);
    orderItem.setCount(count);

    item.removeStock(count);
    return orderItem;
    }

public void cancel(){ // 주문취소시 재고를 더한다
getItem().addStock(count);
}

—

  • 도메인 모델 패턴과 트랜잭션 스크립트 패턴
    도메인 모델 패턴은 대부분의 비즈니스 로직이 엔티티에 속해있고, 서비스는 역할을 위임하고 트랜잭션 바인딩 정도로 사용되것이다
    트랜잭션 스크립트 패턴은 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 말한다

  • 수정에 대해서는 ‘영속화 + 필요한 메서드 변경’ 을 사용하던 ‘병합’을 사용하던 상관없다고 말하고 있음

Read more »
1…8910…19

JunYoung Park

182 posts
18 categories
344 tags
RSS
© 2020 JunYoung Park
Powered by Hexo v3.6.0
|
Theme – NexT.Muse v7.1.0