enum을 쓰기 전에, 아래 예제 코드에서

코드 1번은 현금, 코드 2번은 카드라는 정보가 DB의 "결제 코드" 테이블에 따로 있는 경우, 아래와 같이 소스코드 단에서 코드를 변환해버리면 DB layer와의 의존성이 생겨버린다. 그런 경우 enum을 쓸게 아니라, DB 테이블에 있는걸 읽어와서 처리해야 의존성을 제거할 수 있다.


매번 그냥 조인하는 식으로 처리해도 되지만 그 보다는 

1. DBMS function을 이용한 코드 변환을 사용하거나, 

2. Redis같은걸로 코드 테이블을 메모리에 올려두고 이를 참조해서 사용하는 것이 좋다.



```java

@Getter

public enum PaymentCode {

    CASH(1, "현금", "cash"),    // 여기서 세번째 인자는 별로 필요없지만 이렇게도 가능하다는 것을 보여주기 위해.

    CARD(2, "카드", "card");


    private final int code;

    private final String koName;

    private final String enName;


    PaymentCode(int code, String koName, String enName) {

        this.code = code;

        this.koName = koName;

        this.enName = enName;

    }


    public static PaymentCode fromKoName(String text){

        for(PaymentCode code : PaymentCode.values()){

            if (code.getKoName().equals(text)) {

                return code;

            }

        }

        throw new IllegalArgumentException();

    }

}

```


```java

/* 기본 제공 */

log.info("{}", Payment.PaymentCode.valueOf("CARD"));     // CARD

log.info("{}", Payment.PaymentCode.CARD);                // CARD

log.info("{}", Payment.PaymentCode.CARD.name());         // CARD

// Payment.PaymentCode.values() 이 것도 있다.


/* @Getter */

log.info("{}", Payment.PaymentCode.CARD.getEnName());    // card

log.info("{}", Payment.PaymentCode.CARD.getKoName());    // 카드

log.info("{}", Payment.PaymentCode.CARD.getCode());      // 2


/* 직접 정의. */

log.info("{}", Payment.PaymentCode.fromKoName("현금"));   // CASH

```


JSON Mapping은 다음과 같이 보내면 매핑된다.

```js

{

    "PaymentCode타입의 필드명" : "CASH"

}

```



 

spring security에서 "사용자"를 나타내는 인터페이스는 `` UserDetails``가 있음.

기본적으로 이를 구현한 `` User``라는 클래스가 제공되고 별다른 설정 없이 inmemory에 계정을 등록하거나 하는 테스트 용도의 코드에서는 자동으로 스프링에서 `` User`` 클래스를 사용하는 듯.

 

아무튼 요구사항에 따라 email이라던지.. id라던지 추가적인 정보를 저장해야 할 것 이므로 `` UserDetails``를 implements한 모델 클래스(Account)로 정의하거나, `` User`` 클래스를 extends한 클래스를 정의해도 된다.

`` User`` 클래스를 확장하는 경우 중복 세션 방지 등의 기능을 자동으로 처리해줘서 편리한 부분이 있다.

```java

@ToString(callSuper=true)
@Getter
@Setter
public class Account extends User {
    private long branchId;

    public Account(String username, String password, long branchId,
        Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
        this.branchId = branchId;
    }
}

```

Role과 Authority는 분명히 쓰임이 다르다.
그러나 UserDetails에서는 둘 다 Collection<? extends GrantedAuthority> authorities에 들어간다.

spring security는 이를 구분하기 위해 hasRole()과 hasAutority()로 둘을 구분해서 메서드를 제공하고 있고, Role은 가져올 때 ROLE_ 접두사를 자동으로 붙여준다. 그래서 애초에 authorities에 삽입할 때 Role이면 ROLE_ 접두사를 붙여서 삽입해야 한다.

 

그리고 로그인에 대한 비즈니스 로직은 `` UserDetailsService``를 구현한 서비스 클래스를 만들어서 처리한다. 예를 들면 `` loadUserByUsername``같은 메서드. DAO를 이용해서 Account를 가져와서 반환해주는 작업.

이거는 DB에서 가져올거라면 꼭 구현해야 하는게 어느 테이블에서 계정 정보를 검색할건지를 못찾으니까. 여기서 DAO를 직접불러서 가져온다.

https://github.com/umbum/convenience-store-POS-system/blob/develop/project/src/main/java/com/umbum/pos/service/AccountService.java

 

그리고 여기서 Service에서는 DAO(Repository)에 접근해서 Account를 가져올건데, DAO가 다 그렇지만 DB에서 꺼내온걸 객체에 할당할 때 타입 문제에 주의한다. 특히 오라클의 BigDecimal같은 것...

https://github.com/umbum/convenience-store-POS-system/blob/develop/project/src/main/java/com/umbum/pos/repository/AccountRepoImpl.java

 

 

`` AuthenticationProvider``는 넘어온 username/password에 해시(Bcrypt)를 적용하고 DB에 있는 정보랑 일치하는지 판단해서 인가하는 작업을 처리한다. 

따라서 이러한 작업에 대한 커스터마이징(예를 들면 password 뿐만 아니라 지점 번호도 검증을 할거라던가.)이 필요하지 않다면 구현하지 않고 그냥 스프링에서 처리하도록 놔둬도 된다.

 

 

로그인 성공 / 실패에 대한 콜백을 커스터마이징 해야 한다면

AuthenticationSuccessHandler  /  AuthenticationFailureHandler

 

제일 기본적인 예제

 

좀 더 커스텀한 예제. AuthenticationProvider

 

 

다음 설정을 안하면 모든 POST가 /login 쪽으로 포워딩되니까 개발 시에는 다음 설정을 수행해주고, 

실제 서비스 시에는 csrf token을 페이지에 hidden으로 넣어서 줄 것.

```java

http.csrf().disable();

```

 

로그인한 사용자 정보를 가져오기

https://github.com/thymeleaf/thymeleaf-extras-springsecurity  

타임리프에서 현재 로그인한 유저 이름을 가져온다던가, 로그인한 유저 권한을 가져온다던가,

ROLE_에 따라서 어떤 DOM을 띄워줄지 말지 등등을 컨트롤하려면 이걸 쓴다.

 

백엔드에서  현재 로그인한 사용자 정보 가져오기    

 

ACCOUNT, AUTHORITY 테이블 예제

```sql

CREATE TABLE ACCOUNT(
    USERNAME VARCHAR2(32) NOT NULL,
    PASSWORD VARCHAR2(128) NOT NULL,
    BRANCH_ID NUMBER(18) NOT NULL,
    CONSTRAINT PK_ACCOUNT PRIMARY KEY(USERNAME)
);
CREATE TABLE AUTHORITY(
    USERNAME VARCHAR2(32) NOT NULL,
    AUTHORITY VARCHAR2(32) NOT NULL,
    CONSTRAINT FK_AUTHORITY FOREIGN KEY(USERNAME)
           REFERENCES ACCOUNT(USERNAME)
);

```

 

 

  • 에러 : 애초에 예상이 불가능한 것.
  • 예외 : 발생을 예상할 수 있는 것. 그리고 예상할 수 있기 때문에 그에 대한 대비로 try-catch가 있는 것.
    • checked exception : 컴파일 타임에 경고를 해줘서 예외 처리가 안되어 있으면 컴파일이 안되는 예외.
      이걸 컴파일 타임에 발생하는 Exception이라고 말하면 좀 그렇다. 정확히는 Exception은 Runtime에 발생하며 컴파일 타임에 경고를 해주는거지. 대표적인게 IOException, SQLException
    • unchecked exception : 컴파일 타임에 경고를 안해준다. 대표적인 것이 NPE, IndexOutOfBoundsException

      자바에서는 `` RuntimeException``과 그 하위 예외들이 unchecked exception, 나머지는 checked exception이다.

 

예외를 어떻게 처리할지 생각하기 전에 항상, 예외를 마주쳤다는 것은 무엇을 의미하는가?를 먼저 생각해야 한다.

1. Exception은 조건문이 아니다. Exception을 if처럼 쓰지 마라.

즉, 조건의 의미가 들어있는게 아니다. 말 그대로 예외다.

의미 상 조건에 맞게 처리해야 하는 경우는 if, 예상치 못한 상황이 왔을 때 어떻게 행동할 것인가는 try - except로 처리한다.

 

조건문은 예상된 flow대로 흘러가는 반면 예외는 try 안의 코드를 실행하다 언제 실행 흐름이 except로 넘어갈지 모른다는 점도 생각해 보면 둘을 구분하는데 도움이 된다.

 

예를 들어서 argv의 개수가 적을 때라던가, 이런 예상할 수 있는 상황은 그냥 일단 argv[0]에 접근하고 안되면 except로 받는 것 보다, ``c if``로 length check를 하는게 더 나아보인다.

 

[*] ``py if`` 안에 ``c return 0``이런 것 대신 ``py raise`` 쓰는 것은 좋다.  이거랑 위에 적은 내용은 별개임.

특히 문자열 등을 리턴해야 하는 경우 예외 상황에서 리턴으로 처리하게 되면 리턴값을 뭐로 해야하나 애매할 때가 있다. raise 쓰면 됨.

 

2. API에서 발생하는 예외는 "이렇게 쓰지 마라."를 의미할 수도 있다.

예를 들면 ``java JdbcTemplate.queryForObject()``는 DB에서 가져온 데이터가 없으면(null) 예외를 발생시킨다.

근데 이 메서드는 애초에, 꼭 이 데이터를 null 없이 받아야 한다라고 설계되어 있는 메서드다.

그래서 이렇게 없는 데이터를 가져올 수 있는 케이스가 예상이 된다? 라면 예외가 발생하지 않는 다른 메서드를 사용하고 `` if``로 처리하는 것이 자연스럽다.

 

3. Global Exception Handling은 모든 예외를 Global Exception Handler에서 처리하라고 있는 것이 아니다!

예를 들어 위 2번과 같은 상황에서, "어떤 입력에 대해서는 queryForObject()에서 Unchecked Exception이 발생할 것임을 알지만 그 것을 감안하고서라도 이 메서드를 꼭 사용해야 한다."와 같은 케이스를 생각해보자.

이미 Global RuntimeException Handler가 등록 되어 있다고 하더라도, 굳이 이 Exception을 그리로 보내야 할 이유가 있는가? 

오히려 여기서 잡아서 pass하고 null을 리턴하는게 맞을지도 모른다.

이런 경우 해당 메서드 내에서 try-catch로 예외에 대한 적절한 처리를 하는게 맞을 수 있다.

 

즉, Global Exception Handling은 한 가지의 선택지가 될 수 있는 것이지 모든 처리를 반드시 여기서 하라는 얘기는 아니다.

 

Global Exception Handler를 사용하면 좋은 케이스는 다음과 같다.

  • `` RuntimeException``에 대한 DefaultHandler를 지정할 때. (handleDefaultRuntimeException)
    NPE같은 일일히 예상할 수 없는 unchecked exception에 대해 DefaultHandler를 지정하게 되면 일관된 응답을 기대할 수 있다.
    로깅도 용이하고 갑자기 프로그램이 뻗는 상황도 방지할 수 있을 것이다.
  • 여러 곳에서 비슷한 예외가 발생하고 이에 대해 일관된 처리가 필요하거나, 일관된 응답이 필요할 때.
    • 최상위 unchecked인 RuntimeException에 대한 핸들러가 이미 존재한다고 해도, 그 자식인 다른 unchecked에 대한 핸들러를 지정해서 조금 더 구체적으로 예외 상황을 처리하는 것이 필요할 때가 많다.
    • 물론 checked에 대해서도 가능함.
  • checked exception과 unchecked exception에 대한 처리 로직을 한 곳으로 모으고 싶을 때.
  • try-catch를 하는 부분이 지저분하거나, throws를 해서 메서드 시그니처가 지저분해지는 것을 막을 때

 

checked exception에 대해서 Global Exception 처리를 하려면 약간의 트릭을 사용한다. 

checked exception은 반드시 try-catch나 상위로 throws 둘 중 하나를 해야 하기 때문에, checked exception을 처리하는 척 하면서 unchecked exception으로 바꿔야한다.

 

RuntimeException을 상속 받은 RuntimeIOException 클래스를 하나 만들고,

try-catch로 감싼 다음 RuntimeIOException에 checked exception을 담아서 던지도록 한다. (stack trace에 남는다.)

```java

try {

    // IOException 발생 ( checked exception )

} catch (Exception e) {

    throw new RuntimeIOException(e)       // this is unchecked exception!!!

}

```

```java

/* global exception handler */

@ExceptionHandler(RuntimeIOException.class)

public void handleRuntimeIOException(final RuntimeIOException runtimeIOException)

```

 

* assert는 내부적인 정확성을 보장하는데 사용한다.

``py assert``는 내부적인 정확성을 보장하는 데 사용된다.

반드시 어떤 값을 입력 해야만 한다는 느낌으로 정확한 사용법을 강요할 때 쓰는게 아니다.

마찬가지로 예상치 못한 값이 들어왔으니까 exception을 발생시킨다!! 이런 느낌으로 쓰는게 아니다. (이런 경우는 ``py if - raise``를 사용한다.)

따라서 argument로 알맞은 값이 들어왔는지 검증할 때는 사용하지 않는다.

 

 

+ Recent posts