728x90

기본적인 흐름

  1. 서버에서 ssh-keygen -t rsa -m pem 명령어로 key pair를 생성함
  2. 특정 이름을 지어주지 않는한 id_rsa, id_rsa.pub 이렇게 두 개가 생성될 것이다. 각각 private key, public key 이다.
    public key인 id_rsa.pub을 mv id_rsa.pub authorized_key 로 등록 해준다.
    그리고 private key인 id_rsa를 mv id_rsa id_rsa.pem 으로 pem 키를 만들어준다. 만들어진 pem키는 접속할 때 필요한 키이다.
  3. ssh-add {key가 존재하는 경로와 key명} 로 해당 key를 등록해준다. 그리고 ssh-add -l 으로 잘 등록 되었는지 확인이 가능하다.
  4. ssh 로 원하는 서버에 접속한다.

 

삽질

 
1. 

WARNING: UNPROTECTED PRIVATE KEY FILE!
혹시 이런 에러 뜨면 다음 명령어 입력 하면 된다.

chmod 600 {key 명}

 
 
 
2. ssh 로 서버에 접속할 때 비밀번호입력을 요구하면 ~/.ssh/ 에 id_rsa.pem 혹은 id_rsa.pub 키가 없다는 뜻이다.
 
 
3. enter passphrase for key”가 뜨는 이유는 키 페어를 생성할 때 Phrase를 입력해서이다. 이걸 안뜨게 하고 싶으면 해당 구문을 입력하는 단계에서 그냥 엔터를 치면 된다.
 
 

느낀점

개인 클라우드 서비스를 만들어 보려고 NAS용 컴퓨터를 구입해서 이것저것 시도중이다.
이번 서버접속하는 단계를 조작해보면서 예전에 학부때 들은 시스템클라우드보안 수업에서 들은 RSA, Diffie-hellman 등 수업 내용들이 다는 아니지만 생각나고 추억들이 떠올랐다. 기분 좋은 옛 생각이 떠오르는 시간이었다.
그리고 지금껏 AWS, NCP 등 이미 만들어진 클라우드 서비스만 사용해오다가, 이런 클라우드 서비스를 직접 만들어 보니 평소에 사용하고 있던 서비스들이 어떤 방식으로 만들어 졌을거고, 해당 서비스들을 처음 개발하고 관리하는 사람들은 어떤 고민을 하면서 만들었고 유지보수 하고 있는지 더 깊게 상상해보고 고민해보는 시간이었다.
 

728x90

회사에서 꽤 크고 중요한 프로젝트를 맡았다.

 

회사에서 최초로 JPA를 도입했다. (회사에서는 Mybatis만 사용하고 있었다)

 

ORM이 확실히 객체지향적으로 코드가 깔끔하게 나오는 느낌이었다. 그런데 문제가 있었다.

 

기존에 Mybatis만 사용하고 있어서 단일로 DataSourceTransactionManager를 사용중이었는데, JPA를 도입하면서 JpaTransactionManager 를 추가 해야 했다.

 

기존에 DataSourceTransactionManagerJpaTransactionManager bean을 @Primary 로 하고 테스트 코드로 Mybatis와 JPA 혼합해서 DML을 해보았다.

    1. Mybatis로 select
    2. JPA로 insert
    1. JPA로 select
    2. Mybatis로 insert

잘 되었다.

그러나 롤백에서 문제였다.

    1. Mybatis로 select
    2. (예외 throw)
    3. JPA로 insert
  • 문제
    1. JPA로 select
    2. (예외 throw)
    3. Mybatis로 insert

Mybatis로 insert할 때 롤백이 안되는 문제였다.

 

프로젝트에 기존에 있던 로직들이 다 Mybatis로 되어 있는데, 현재 상태로는 @Primary로 되어 있어 있는 JpaTransactionManager를 사용하고 있어서 기존 로직들이 롤백이 안됐다.

 

그래서 JPA에는 JpaTransactionManager를 사용 해야 하고, Mybatis에는 DataSourceTransactionManager를 사용 하여 트랜잭션을 관리 하게끔 해야 했다.

 

여러 대안이 있었다.

  • ChainedTransactionManager
  • JtaTransactionManager
  • @Primary를 DataSourceTransactionManager 로 변경하고 JPA 로직에만 @Transactional 에 트랜잭션 매니저 명시

ChainedTransactionManager는 후 순위로 커밋하는 트랜잭션에서는 예외가 발생하면 이전 트랜잭션에서 커밋된 부분은 롤백이 안되는 한계가 있었고, 어느 한 쪽의 커넥션 갯수가 부족해서 타임아웃이 발생할 가능성도 우려 됐다. 무엇보다 해당 트랜잭션 매니저는 단순 순차적으로 커밋하는 것일 뿐 2PC가 아니라 도입하기 걱정스러웠다.

 

JtaTransactionManager는 XA 프로토콜을 사용한 완벽한 2PC로 구현되어 있었다. 그러나 락이 잡히는 시간이 길어질 것 같고, 여러번 디비랑 커넥션을 맺어 퍼포먼스에 이슈가 있을 것 같았다.

 

그래서 @Primary를 DataSourceTransactionManager 로 변경하고 JPA 로직에만 @Transactional 에 트랜잭션 매니저 명시를 해서 처리했다.

 

 

마무리

이번 이슈로 더 성장한 느낌이 들어 좋다. 

 

고민하는 스펙트럼이 넓어졌다.

728x90

현재 회사에서 한 프로젝트에서 spring framework 4.2.8을 사용하고 있다.

 

해당 프로젝트에서 @RequiredArgsConstructor 으로 빈 주입을 했는데 아래와 같은 에러가 났다.

Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field:

 

그래서 찾아보니 4.3 버전 부터 가능한 방법이었다.

 

 

결국엔 @Autowired로 대체할 수 밖에 없었다.

 

 

결론

해당 프로젝트 버전업을 해야겠다.

728x90

쿠버를 세팅 하면서 생긴 이슈이다.

 

sudo apt-get update

후에 생긴 에러다

 

해결법

sudo mkdir -p /etc/apt/keyrings

 

echo "deb [signed-by=/etc/apt/keyrings/kubernetes.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list

 

curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes.gpg

 

 

참고

https://github.com/kubernetes/release/issues/2862#issuecomment-1533888814

728x90

문제사항 제시

이러한 에러를 맞이했다. Boolean 타입과 Timestamp은 비교대상이 안된다고 한다.

 

 

해결책 모색

나의 코드에서 Boolean 타입 혹은 Timestamp을 비교하고 있는 로직을 찾아보았다.

이 부분이 뭔가 잘못된 것 같다. 논리적으로 잘못된 것이 없어보이는데 아마 추측으로 '&&' 부분이 잘못된 것 같았다. 그래서 찾아보니 '&&'는 'AND'의 약어이고 같은 기능을 하지만 'AND'는 SQL 표준 연산자로 모든 데이터베이스 시스템에서 지원된다고 한다. (이식성이 좋은 것 같다.) 그래서 'AND'로 적용해보았다.

 

 

적용

 

 

결과

결과적으로 이제 해당 에러는 해결되었다. ㅎㅎ

 

728x90

문제사항 제시

오버부킹 이슈를 해결하려고 PESSIMISTIC_WRITE 락을 분명 걸었는데 락 적용이 되지 않았다. 

 

코드로 예시를 들어보겠다.

 

Customer 엔티티 코드이다.

@Entity
@Table(name = "customer")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Getter
public class CustomerEntity extends BaseEntity {

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

    private String name;
    private String email;
    private String password;
    private String tel;

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ScheduleEntity> schedules = new ArrayList<>();

    ...

 

Schedule 엔티티 코드이다.

@Entity(name = "schedule")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString(exclude = {"studySeat", "customer"})
@Where(clause = "state = 'RESERVED' && started_time >= NOW()")
public class ScheduleEntity {

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

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH)
    @JoinColumn(name = "customer_id", referencedColumnName = "id")
    private CustomerEntity customer;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH)
    @JoinColumn(name = "study_seat_id", referencedColumnName = "id")
    private StudySeatEntity studySeat;

    @Column(name = "started_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime startedTime;

    @Column(name = "end_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime endTime;

    @Enumerated(value = EnumType.STRING)
    private ScheduleState state;
    
    ...

 

StudySeat 엔티티 코드이다.

@Entity
@Table(name = "study_seat")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Getter
public class StudySeatEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String seatNumber;
    private boolean occupied;

    @OneToMany(mappedBy = "studySeat", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ScheduleEntity> schedules = new ArrayList<>();
    
    ...

 

StudySeatService 코드이다.

    public CommandReserveSeatResponse reserveSeat(final ReserveSeatRequest command, final long studySeatId) {
        Customer customer = findByCustomerId(command.getCustomerId());
        StudySeat studySeat = findByStudySeatId(studySeatId);

        check(studySeat.isReservable(command.getStartedTime(), command.getEndTime())
            , INVALID_SCHEDULE_RESERVATION_ALREADY_OCCUPIED)
        ;

        studySeat.reserve(
            customer, studySeat,
            command.getStartedTime(), command.getEndTime()
        );

        studySeatRepository.save(studySeat);

        return new CommandReserveSeatResponse(success, studySeatId);
    }

 

 

Customer, StudySeat은 Schedule과 1:M 관계이다. DDD 구조상 StudySeat 과 Schedule로 하나의 aggregate로 묶여져 있고 StudySeat이 Schedule을 제어하는 역할을 한다.

 

그래서 이와 같은 파일 구조를 가진다.

 

그래서 결론은 StudySeat의 Repository에 findById에 Lock을 걸었는데 적용되지 않았다.

여러개의 쓰레드를 만들어서 한꺼번에 실행 시키는 테스트 코드를 실행시켰는데, 콘솔에 찍히는 query문을 여러번 눈을 씻고 찾아봐도 select for update가 계속 찍혔는데도 내가 원했던 결과가 나오지 않았다.

 

 

해결책 모색

생각 해보니 Service 코드를 보면 Customer 부터 찾아오는 것을 확인할 수 있었다.

    public CommandReserveSeatResponse reserveSeat(final ReserveSeatRequest command, final long studySeatId) {
        Customer customer = findByCustomerId(command.getCustomerId());
        StudySeat studySeat = findByStudySeatId(studySeatId);

이 순서대로면 CustomerRepository에 락을 적용해야 원하는 결과가 나온다.

 

왜냐하면 Customer와 Schedule도 1:M으로 매핑되어 있기 때문에 Customer먼저 찾으면 

 

이런꼴이 되기 때문에 isReservable 메소드(예약이 가능한지 검증하는 메소드)에 들어가면 schedule이 비어 있어서 모두 통과가 되는 것이다. 그러면 락이 걸리는 StudySeat 부터 검색하게 하면 해결이 된다.

 

 

적용

    public CommandReserveSeatResponse reserveSeat(final ReserveSeatRequest command, final long studySeatId) {
        StudySeat studySeat = findByStudySeatId(studySeatId);
        Customer customer = findByCustomerId(command.getCustomerId());

 

결과

Customer를 먼저 찾아오게 되면 여러개 쓰레드 모두가 매핑된 Schedule이 비어있다고 인식된다. 그래서 락이 걸리는 StudySeat 객체 부터 찾아오게 하면서 이 문제를 해결했다. 락을 걸 때 순서 또한 중요하다고 느꼈다.

728x90

문제사항 제시

나는 테스트 환경에서 h2 in-memory db를 사용하고 있었다. 통합 테스트를 하려고 했는데 에러가 몇시간 동안 발목을 잡았다.

 

ddl-auto: validate를 해놓은 상태이고 엔티티와 디비가 맞는지만 확인했다.

 

그런데 Flyway가 실제 디비에 어떻게 적용시켰는지를 알아야 엔티티를 디비에 맞추든, 디비를 엔티티에 맞추든 할 수 있었다.

(사실 엔티티를 디비에 맞추면 OCP 위반이기 때문에 엔티티를 디비에 맞추어야했다.)

 

그렇게 하려면 디비의 상황을 알아야 했는데 아무리 h2.console.enabled=true 로 해놓아도 http://localhost:8080/h2 가 열리지 않았다.

 

 

해결책 모색

애초에 나는 application-test.yml, application.yml을 나눠놓았다.

 

그래서 application.yml 에서

 

spring:
  profiles:
    active: test

 

를 하고 애플리케이션을 실행시켜서 h2 console에 들어갈 수 있었다.

 

그 결과, 실제 어떻게 Flyway가 디비에 적용시켰는지, 왜 jpa가 매핑을 못시키고 있었는지 원인을 알 수 있었다.

 

 

원인

테이블들이 기본적으로 uppercase로 적용 되어 있었다.

 

적용

원인을 해결하기 위해 

 

datasource:
  url: jdbc:h2:mem:skka-for-test;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE

 

결과

 

맨 뒤에

;DATABASE_TO_LOWER=TRUE

을 추가 해주니까 됐다.

728x90

프로젝트 내용 및 결과물

https://github.com/f-lab-edu/SSKA

 

GitHub - f-lab-edu/SSKA: 스터디 카페 운영 서비스 입니다.

스터디 카페 운영 서비스 입니다. Contribute to f-lab-edu/SSKA development by creating an account on GitHub.

github.com

 

사용한 NCP 서비스

  • SourceCommit
  • SourceBuild
  • SourceDeploy
  • SourcePipeline
  • Cloud DB for MySQL
  • Pinpoint Cloud
  • Auto Scaling
  • Load Balancer
  • Server
  • Object Storage
  • FileSafer

 

Ncloud 사용 중 느낀 점 (좋았던 점, 바라는 점 등)

처음에 제공된 크레딧으로 많은 기능들을 사용해 볼 수 있었다. 아마 2년 전쯤 AWS로 간단하게 배포한 경험이 있었는데 그때의 기억을 되새겨 보면 AWS는 UI가 투박스럽다고 느낀 기억이 있다. 네이버 클라우드는 내가 한국인이라 네이버를 많이 애용해서 그런지 모르겠으나 친근한 UI이고 UX또한 좋아서 이것저것 설정하는데 한결 편했다.

 

그리고 공식문서가 너무 잘 되어 있어서 꼼꼼히 잘 읽으면서 필요한 기능들을 설정하니 모든 기능들을 잘 적용할 수 있었다. 이번에 NCP 클라우드 환경 설정을 하면서 블로그를 거의 찾아보지 않았던 것 같다.

 

그러나 깜짝 놀랬던점은 FileSafer라는 기능이 있는데 이 기능에서 약 하루만에 9만8천원이 나가버렸다ㅠㅠ SourceCommit에 푸시한 코드들이 바이러스가 없는지 검사해주는 기능이어서 좋아보여서 설정을 해두었는데 돈이 너무 많이나가 버렸다. 피같은 내 돈...아무리 크레딧이지만 나한테는 정말 귀했다.. 10만원, 30만원 크레딧이 있었는데 10만원 크레딧이 다 소진 되었다고 아침에 문자가 와서 어안이 벙벙하여 NCP 크레딧 현황을 봤는데 너무 허무했다. 그치만 9만8천원에 인생공부 했다고 생각했다. 개발도 인생에 포함이 되니 인생 공부라고 하겠다.

MySQL 서버를 띄었는데 돈이 너무너무 많이 나갔다. 하루에 거의 2만원쯤 나갔나..? 기억이 안난다. 그래서 바로 디비 인스턴스를 내린 기억이 있다. 지금도 핀포인트랑 디비 성능 테스트할 때만 디비 인스턴스 올려서 테스트 하고있다.

 

 

프로젝트를 진행하며 재밌었던 점

이번 SKKA 프로젝트를 하며 다양하게 로직을 구현 해보기도 하고, Restful 하게 API도 만들어보고 객체지향을 빡세게 적용 시켜보기도 하고 클라우드 환경 구성과 CI/CD 적용을 하는 등 정말 다양하게 많이 해보았다. 특히 재미있었던 부분이 객체지향적인 설계였다. 코딩을 하면서 "집청소"하는 느낌이 든 적은 이번이 처음이었다. 난잡하고 가독성이 떨어지면서 객체들의 역할을 생각하며 코딩을 했다. 처음에 Service 코드에 로직을 구성하고 리팩토링 작업을 시작하면서 각각의 도메인 객체가 맡은 역할을 생각하여 비즈니스 로직으로 집어 넣는 작업들이 재미있었다. 마치 어릴 때 좋아했고 지금도 좋아하지만 하지 못하고 직업을 갖고 하려고 하는 레고조립 같았다.

 

 

프로젝트를 진행하며 어려웠던 점

스터디카페 예약 시스템을 구축하는 단계에서 검증 해야 하는 케이스가 생각 할수록 계속 나왔다. 그래서 총 9가지가 나왔었다. 이것을 검증하려고 몇일을 고민했던 것 같다.

프로젝트 wiki에 트러블슈팅들은 써 놓았다.(https://github.com/f-lab-edu/SSKA/wiki/04.-Trouble-Shooting)

 

 

프로젝트를 진행하며 아쉬웠던 점

작년 인턴 생활을 했을 때 알람 도메인을 맡아서 개발한 경험을 살려 푸시알람도 멋있게 개발해보고싶다는 아쉬움이 남았다.. 추후 붙여볼 예정이다.

 

 

사용한 기술 스택

 

 

앞으로 Ncloud를 어떻게 활용할지

앞으로 많은 프로젝트에서도 많이 사용될 것 같다. 특히 좀 더 전문적으로 핀포인트를 사용해서 애플리케이션을 더 개선해보고싶은 마음이 많다. 그리고 이번에는 VPC 환경이었다면 다음에는 classic 모드로 클라우드 환경을 조성해 볼 예정이다.

728x90

문제

프로젝트를 하면서 데이터베이스 id, password, secretKey 등은 깃헙 레포지토리에 노출되면 공격자가 쉽게 서버의 중요한 정보를 알아내어 서버가 위험해 빠질 수 있다. 이것을 방지하기 위해 env.properties 파일을 만들어서 중요 정보들을 관리를 했는데 프로젝트의 비즈니스 로직과 관계없는 클래스를 생성하여 환경설정을 해줘야했다. 그래서 개선을 해보았다.

문제 해결

문제를 해결하기 위해 OS자체내에 환경변수를 설정했다.

터미널이 실행되면 .bash_profile 파일 부터 읽는다고 한다.

 

1. vi ~/.bash_profile

위 명령어를 터미널에서 입력하여 export PORT=8080 이런식으로 원하는 변수들을 설정해준다.

설정이 완료 됐으면 wq로 저장 하고 나간다.

 

 

2. source ~/.bash_profile

위 명령어로 수정된 값들을 바로 적용 시켜보자

 

 

3. 테스트 하기

echo $변수 를 사용하여 잘 나오는지 테스트 해주자.

 

아래와 같이 나온다면 정상적으로 적용 되었다는 뜻이다.

 

 

4. 코드에 적용하기

테스트 까지 완료 후 코드에 적용해보자.

나는 IntelliJ IDE로 스프링부트 프로젝트를 하고 있는데, application.yml 설정 파일에 

이렇게 적용하여 애플리케이션을 실행해 보면 환경변수들이 잘 들어가며 잘 실행 된다.

728x90

개요

NCP에 API요청을 보내기 위해서는 시그니처, 타임스탬프, 시크릿키, 엑세스키 등이 필요하다. 여기서 시그니처와 타임스탬프가 얻기가 조금 까다로웠다. 그래서 다른 분들은 이런 시행착오를 겪지 마시라고 이렇게 적어본다.

 

시그니처 생성

네이버 클라우드 플랫폼에서 제공하는 API 공식 문서에서 제공하는 Bash 스크립트이다. 이것을 사용해서 시그니처를 생성하자

function makeSignature() {
	nl=$'\\n'

	TIMESTAMP=$(echo $(($(date +%s%N)/1000000)))
	ACCESSKEY="{accessKey}"				# access key id (from portal or Sub Account)
	SECRETKEY="{secretKey}"				# secret key (from portal or Sub Account)

	METHOD="GET"
	URI="/photos/puppy.jpg?query1=&query2"

	SIG="$METHOD"' '"$URI"${nl}
	SIG+="$TIMESTAMP"${nl}
	SIG+="$ACCESSKEY"

	SIGNATURE=$(echo -n -e "$SIG"|iconv -t utf8 |openssl dgst -sha256 -hmac $SECRETKEY -binary|openssl enc -base64)
}

{accessKey}, {secretKey}는 각자 계정에서 발급 받은 키를 넣는다.

그리고 URI에는 공식 문서에서 각자 원하는 요청을 찾는다. 나는 SourceBuild에 "빌드 시작" API를 사용하고 싶어서 찾은 결과 밑에와 같다.

API URL에 보면 {SOURCEBUILD_API_URL}이 보일 것이다. 이것은 각 API의 개요에 들어가 보면 나와 있다.

https://sourcebuild.apigw.ntruss.com/api/v1  <- 이게 {SOURCEBUILD_API_URL} 가 된다.

 

이제 위에 Bash로 돌아가서 그러면 URI에 무엇을 넣어야 할것인가?

바로 

/api/v1/project/{projectId}/build

이거를 넣어야 한다. (여기서 많이 헤맸다)

시그니처만 잘 생성 됐다면 바로 완성 코드를 보자.

 

완성 코드

function makeSignature() {
	nl=$'\\n'

	TIMESTAMP=$(echo $(($(date +%s%N)/1000000)))
	ACCESSKEY="{accessKey}"				# access key id (from portal or Sub Account)
	SECRETKEY="{secretKey}"				# secret key (from portal or Sub Account)

	METHOD="GET"
	URI="/photos/puppy.jpg?query1=&query2"

	SIG="$METHOD"' '"$URI"${nl}
	SIG+="$TIMESTAMP"${nl}
	SIG+="$ACCESSKEY"

	SIGNATURE=$(echo -n -e "$SIG"|iconv -t utf8 |openssl dgst -sha256 -hmac $SECRETKEY -binary|openssl enc -base64)

	my_list=("${TIMESTAMP}","${SIGNATURE}")
	
	echo "${my_list[@]}"
}

ret_value=$(makeSignature)

IFS=',' read -ra vStr <<< "$ret_value"

curl -i -X POST \
-H "x-ncp-apigw-timestamp:${vStr[0]}" \
-H "x-ncp-iam-access-key:{accessKey}" \
-H "x-ncp-apigw-signature-v2:${vStr[1]}" \
'https://sourcebuild.apigw.ntruss.com/api/v1/project/{projectId}/build'

 

이렇게 하면 API call이 된다. 

 

※ 그리고 시그니처를 생성할 때 TIMESTAMP와 API 요청할 때 넣는 TIMESTAMP가 같아야 한다.

 

 

참고

- https://api.ncloud-docs.com/docs/common-ncpapi

- https://api.ncloud-docs.com/docs/devtools-sourcebuild-build

+ Recent posts