반응형

아래와 같이 @RestController  에 메소드가 있다고 생각해보자.

@PostMapping
public ResponseEntity<AccountResDto> getResDto(@RequestBody AccountReqDto dto){
    return ResponseEntity.ok(AccountResDto.builder().email("test@test.com")
    .loginId("test")
    .username("tester")
    .build()
  );
}

Post 요청에 대해서 AccountReqDto 라는 Object 형태의 파라메터를 받아서 Response 를 리턴하는 메소드 이다.

AccountReqDto 는 다음과 같이 되어있다.

@Getter
public class AccountReqDto {

    private String loginId;

    @Builder
    public AccountReqDto(String loginId) {
        this.loginId = loginId;
    }
}

그리고 나서 난 아무 생각없이 테스트 케이스를 돌렸다.

@Test
public void getResDtoTest() throws Exception {
  MvcResult mvcResult = this.mockMvc.perform(
      post("/api/v1/accounts")
      	.contentType(MediaType.APPLICATION_JSON_UTF8)
      	.content(objectMapper.writeValueAsString(reqDto)))
      .andExpect(status().isOk())
      .andDo(print())
      .andReturn();

  String contentAsString = mvcResult.getResponse().getContentAsString();
  AccountResDto accountResDto = objectMapper.readValue(contentAsString, AccountResDto.class);
  Assert.assertEquals("test", accountResDto.getLoginId());
}

당연히 성공할줄 알았던 테스트 케이스는 실패를 한다. 실패의 원인은 다음과 같다. 

 

Status expected:<200> but was:<400>
Expected :200
Actual   :400

 

아니 왜??? 왜 400 error 가 났을까??

결론부터 말하자면 AccountReqDto 클래스에 기본 생성자가 없기 때문에 위와 같은 에러가 발생 했다. 그렇다면 왜 기본 생성자가 있어야 하나??? 그걸 알기 위해서 찾아보다 보니 Serialization 이 연관이 있다는 것을 찾게 되었다. 

 

Serialization

Serialization 은 Object 의 상태를 byte stream 형태로 변환하는 것을 말한다. Deserialization 은 반대의 경우를 말한다. java  에서 Serialization 을 이용하면 객체를 네트워크를 통해 전송이 가능해진다. 

 

그럼 왜 Serialization에는 No-Argument Constructor 가 필요한걸까? 

To allow subtypes of non-serializable classes to be serialized, the subtype may assume responsibility for saving and restoring the state of the supertype's public, protected, and (if accessible) package fields. The subtype may assume this responsibility only if the class it extends has an accessible no-arg constructor to initialize the class's state. It is an error to declare a class Serializable if this is not the case. The error will be detected at runtime.
During deserialization, the fields of non-serializable classes will be initialized using the public or protected no-arg constructor of the class. A no-arg constructor must be accessible to the subclass that is serializable. The fields of serializable subclasses will be restored from the stream.

(출처 : https://docs.oracle.com/javase/10/docs/api/java/io/Serializable.html)

 

요약을 해보면 클래스의 상태를 초기화 할 때 No-Argument Constructor 를 이용한다는 의미이다. 만약 선언되어있지 않으면 에러가 발생한다고 되어있다. 

 

결과적으로 Serialization 을 위해서는 No-Argument Constructor  가 필요하고 그것을 사용하고 있는 Controller 의 RequestBody 에 들어가는 Object 에도 No-Argument Constructor 가 존재 해야 한다는 의미이다.

 

그래서 위 소스에서 테스트가 성공하기 위해서는 No-Argument Constructor 를 만들어 주거나 Lombok 을 사용한다면 @NoArgsConstructor 를 넣어주면 된다.

 

728x90
반응형
반응형

내 github 에 있는 프로젝트들중 예전에 만들었던 건데 내용좀 다시 볼겸 수정을 하고 있었다. 

사실, 간단한거 빼고는 잘 작동을 안하는것 같다.

 

그래서 이번에 라이브러리 버전 업데이트 좀 할겸 소스를 수정을 했다.

 

Spring Boot 버전도 최신 버전으로 수정을 하고 Security 버전도 수정을 했다. 그리고 나서 간단히 로그인을 해보려고 하니 다음과 같은 에러가 발생했다.

 

There is no PasswordEncoder mapped for the id "null"

 

음.. ?

찾아보니 Spring Security 버전이 올라가면서 PasswordEncoder가 변경되면서 발생한 에러였다. 

The general format for a password is:

{id}encodedPassword

Such that id is an identifier used to look up which PasswordEncoder should be used and encodedPassword is the original encoded password for the selected PasswordEncoder. The id must be at the beginning of the password, start with { and end with }. If the id cannot be found, the id will be null. For example, the following might be a list of passwords encoded using different id. All of the original passwords are "password".

(출처 : https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released)

 

그래서 방법은 다음과 같다. (내가 가지고 있는 소스 기준이다. )

 

1. DB 에 password 값을 변경한다. 

 

생성되는 password 값에 "{noop}" 을 붙여서 저장을 한다. 

 

2. UserDetail 서비스를 새로 구현 하였다면 password 부분을 수정해준다. 

    public CustomUserDetails(Account account) {
        this.username = account.getLoginId();
        this.password = "{noop}" + account.getPassword();
    }

 

1번이나 2번이나 결과적으로 password 값 앞에 "{id}" 값을 넣어주면 된다는 의미이다.

 

728x90
반응형
반응형

컨트롤러를 만들어서 Testcase 를 작성한 후에 성공할거라 믿고 돌려봤는데 IllegalArgumentException 이 발생했다.

컨트롤러 코드와 테스트 케이스 코드는 각각 다음과 같다.


UserController.java

1
2
3
4
5
6
@GetMapping(value = "/users/{email}")
public UserDto.Res getUser(@PathVariable @Valid final String email){
    Optional<Users> users = userService.findByEmailValue(email);
 
    return new UserDto.Res(users.get());
}
cs


UserControllerTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Before
public void setUp(){
    users = Users.builder()
            .email(Email.builder().value("test@test.com").build())
            .firstName("TEST")
            .lastName("KIM")
            .password("password")
            .build();
}
 
@Test
public void getUserTest() throws Exception {
 
    given(this.userController.getUser("test@test.com")).willReturn(new UserDto.Res(this.users));
 
    ResultActions resultActions = this.mockMvc.perform(
            get("/users/{email}""test@test.com"))
            .andDo(print());
 
    resultActions
            .andExpect(jsonPath("firstName").value("TEST"))
            .andExpect(jsonPath("lastName").value("KIM"))
            .andExpect(MockMvcResultMatchers.status().isOk());
}
cs


Exception

Caused by: java.lang.IllegalArgumentException: Name for argument type [java.lang.String] not available, and parameter name information not found in class file either.


대체 뭐지??? 맞게 parameter 도 넘겼는데 왜 못찾는거지....



Spring Document 를 보니 다음과 같은 내용을 찾을수 있었다.

The matching of method parameter names to URI Template variable names can only be done if your code is compiled with debugging enabled. If you do have not debugging enabled, you must specify the name of the URI Template variable name to bind to in the @PathVariable annotation.

<출처 : https://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch18s02.html >


컴파일 할때 debugging enabled 가 되어야 동작을 하는데 그렇지 않을 경우에는 반드시 @PathVariable 사용시 name 에 값을 줘야 한다고 되어있다.


그럼 컴파일 할때 debugging enabled 는 뭔가?? -_-;;

java 소스를 컴파일할때 사용되는 javac 명령어를 찾아보면 다음과 같은 옵션이 있다. 컴파일시에 -g 옵션을 사용할 경우 로컬 지역변수(local variables) 를 포함한 debugging information 을 생성하고 디폴트로는 라인 넘버와 소스파일 정보만 생성된다고 써있다.


-g
Generate all debugging information, including local variables. By default, only line number and source file information is generated.
-g:none
Do not generate any debugging information.
-g:{keyword list}
Generate only some kinds of debugging information, specified by a comma separated list of keywords. Valid keywords are:
source
Source file debugging information
lines
Line number debugging information
vars
Local variable debugging information

<출처 : https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html>



그럼 간단한 자바 클래스를 컴파일 해서 확인해보자.


1
2
3
4
5
6
public class Test {
    public static void main (String[] args) {
        String name = "test";
        System.out.println(name);
    }
}
cs

javac Test.java (옵션 없이 할경우)

1
2
3
4
5
6
7
8
9
10
11
12
13
# javap -l Test                                 
Compiled from "Test.java"                       
public class Test {                             
  public Test();                                
    LineNumberTable:                            
      line 4: 0                                 
                                                
  public static void main(java.lang.String[]);  
    LineNumberTable:                            
      line 6: 0                                 
      line 7: 3                                 
      line 8: 10                                
}                                               
cs


javac -g Test.java (-g 옵션을 붙여서 할경우)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# javap -l Test
Compiled from "Test.java"
public class Test {
  public Test();
    LineNumberTable:
      line 40
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   LTest;
 
  public static void main(java.lang.String[]);
    LineNumberTable:
      line 60
      line 73
      line 810
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      11     0  args   [Ljava/lang/String;
          3       8     1  name   Ljava/lang/String;
}
cs


각각의 경우를 javap 명령어로 확인해 보았다. (javap 명령어는 컴파일된 class 파일을 역 어셈블 해주는 명령어 있다. -l 옵션을 붙이면 로컬변수 테이블까지 보여준다.)


첫번째에서는 기본적으로 라인만 보이는데 두번째에서는 로컬 변수까지 다 보여준다. 


결론적으로 맨 처음 Testcase 에서 발생했던 문제는 UserController.java 를 컴파일 했을때 -g 옵션이 안들어가서 컴파일 후에 @PathVariable 에 있는 변수를 못찾아서 발생한 문제였다. 이게 IntelliJ 에서 따로 compile 옵션설정을 해야 할지는 잘 모르겠는데 나는 @PathVariable 에 name 을 명시해주는 방법으로 해결을 했다.


참고자료

https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/spring-path-variable.html

https://objectpartners.com/2010/08/12/spring-pathvariable-head-slapper/

https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/parameter-name-discoverer.html

https://stackoverflow.com/questions/5746894/what-does-the-javac-debugging-information-option-gvars-do

728x90
반응형
반응형

최근에 시간 관련 Data 를 가지고 삽질을 해서 간단히 적어본다.


상황은 이렇다. 

테이블에 특정 Data 를 저장 하고 있었는데 colum 에 언제나 그렇듯 생성 날짜, 시간 형태의 컬럼이 존재하고 있었다. Timestamp type 이었고 Data 는 잘 insert 가 되는 상황이었다. 그런데 이상하게 나는 분명 UTC 기준으로 Data 가 insert 될거라 생각을 했는데 그게 아니었다. Local 기준으로 Data 가 들어가고 있었다. 그러다 보니 이 Data를 사용하는 곳에서 예상치 못한 오류가 발생했다. 


1
2
3
4
5
6
@Test
public void jodaTime(){
    DateTime dateTime = new DateTime(DateTimeZone.UTC);
    System.out.println("dateTime : " + dateTime);
    System.out.println("dateTimeToDate : " + dateTime.toDate());
}
cs


문제가 된 부분은 위에 코드에 toDate() 부분이었다. 위 코드를 실행하면 결과는 아래와 같이 나온다. 


1
2
dateTime : 2019-02-13T07:01:03.407Z
dateTimeToDate : Wed Feb 13 16:01:03 KST 2019
cs


뭔가 좀 이상하다. dateTime 은 UTC 기준으로 나왔지만 toDate 값은 로컬 기준으로 나온다. 그리고 그 값이 DB 로 저장이 되고 있었다. 


Why????


위에 있는 toDate() 함수는 아래와 같이 구현이 되어있다. 그런데 여기에서 저 Date 가 문제다.


1
2
3
public Date toDate() {
  return new Date(getMillis());
}
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Allocates a <code>Date</code> object and initializes it to
* represent the specified number of milliseconds since the
* standard base time known as "the epoch", namely January 1,
* 1970, 00:00:00 GMT.
*
* @param   date   the milliseconds since January 1, 1970, 00:00:00 GMT.
* @see     java.lang.System#currentTimeMillis()
*/
public Date(long date) {
  fastTime = date;
}
 
cs


@see 를 보면 java.lang.System을 참조하고 있다. -_-;. 그런 결과 아무리 앞에서 offset을 설정해봐야 결과값은 로컬에 있는 jvm 의 timezone을 따라간다는 거다. 


만약 DB 에 컬럼 타입이 String 이었다면 이렇게 하면 해결이 되긴 한다.


1
2
3
4
5
DateTime dateTime = new DateTime(DateTimeZone.UTC);
System.out.println("dateTime : " + dateTime);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
simpleDateFormat.setTimeZone(TimeZone.getTimeZone(ZoneId.of("UTC")));
System.out.println("dateTimeToDateFormatter : " + simpleDateFormat.format(dateTime.toDate()));
cs


1
2
dateTime : 2019-02-13T08:07:53.819Z
dateTimeToDateFormatter : 19. 2. 13 오전 8:07
cs


값은 동일하게 나오긴 하지만 그렇다고 DB 컬럼을 바꾸기에는 뭔가 좀 부담스럽다.

그러면 어떻게 하는게 좋을까... 생각을 해보다가 이런 방법을 생각했다.


1. 현재 로컬의 OffsetDateTime 을 구한다. 

2. 그리고 로컬에 있는 Timezone 의 offset을 구한다.

3. offset 에 따른 시간 차이를 분 또는 초로 환산한다.

4. 환산한 값을 현재 로컬 Time 값에서 빼거나 더한다. 


절차가 좀 복잡 하긴 하지만 DB 에는 UTC 값을 저장할 수 있게 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void getUTC(){
    OffsetDateTime now = OffsetDateTime.now();
    System.out.println("현재 시간 : " + now);
 
    // Offset
    ZoneOffset offset = now.getOffset();
    System.out.println("offset : " + offset);
 
    // Offset 을 second 로 변경
    int totalSeconds = offset.getTotalSeconds();
    System.out.println("totalSeconds : " + totalSeconds);
 
    OffsetDateTime offsetDateTime = null;
    if (totalSeconds > 0){
        offsetDateTime = now.minusSeconds(totalSeconds);
    }else{
        offsetDateTime = now.plusSeconds(totalSeconds);
    }
 
    System.out.println("offsetDateTime : " + offsetDateTime);
}
cs


1
2
3
4
현재 시간 : 2019-02-14T13:30:38.592+09:00
offset : +09:00
totalSeconds : 32400
offsetDateTime : 2019-02-14T04:30:38.592+09:00
cs


여기에서 plus 와 minus 할때 totalSecond 값이 음수이냐 양수이냐 에 따라서 반대로 해야 한다. 우리 나라 같은 경우 +9시간이기 때문에 UTC로 맞추려면 빼줘야 한다. 


시간 들어가는 DATA 값은 정말 복잡하고 생각해야 될 것이 많은 것 같다.



728x90
반응형
반응형

Spring Boot 에서 Properties 를 설정하는 방법에 대해서 알아보자.


우선 Properties 파일을 3개를 만들어 보았다.

src/main/resources 하위에 application.properties, application-server1.properties, application-server2.properties 이렇게 3개의 파일을 만들었다.


application.properties

1
2
3
4
application-name: my applicatoin
spring.output.ansi.enabled=always
logging.level.org.springframework.web=debug
server.port=9000
cs


application-server1.properties

1
server.port=9001
cs


application-server2.properties

1
server.port=9002
cs


일단 이런 상태로 어플리케이션을 실행 시켜서 CommandLineRunner 로 서버 포트랑 application name 을 찍어보았다.


1
2
c.p.s.SpringPropertiesApplication        : application Name : my applicatoin
c.p.s.SpringPropertiesApplication        : server port : 9000
cs


찍히는 것은 당연히 application.properties 파일에 있는 내용들이 찍혀 나온다.


그럼 이번에는  application.properties  파일에  spring.profiles.active=server1 이라고 입력해보자.


1
2
c.p.s.SpringPropertiesApplication        : application Name : my applicatoin
c.p.s.SpringPropertiesApplication        : server port : 9001
cs


이번에는 application-server1.properties 에 application-name: my applicatoin 9001 이라고 입력해보자.


1
2
c.p.s.SpringPropertiesApplication        : application Name : my applicatoin 9001
c.p.s.SpringPropertiesApplication        : server port : 9001
cs


지금까지 테스트한 결론은 이렇다.


1. properties 의 profile  을 active 안하면 기본적으로 default properties를 읽는다.

2. active 한 properties 는 default properties의 값들을 override 한다.


In addition to application.properties files, profile-specific properties can also be defined by using the following naming convention: application-{profile}.properties. The Environment has a set of default profiles (by default, [default]) that are used if no active profiles are set. In other words, if no profiles are explicitly activated, then properties from application-default.properties are loaded.

Profile-specific properties are loaded from the same locations as standard application.properties, with profile-specific files always overriding the non-specific ones, whether or not the profile-specific files are inside or outside your packaged jar.


spring boot document 에도 위와 같이 나와있다. 마지막 문장을 번역해보면 다음과 같다.


profile-specific properties 는 기본적인 application.properties 파일처럼 같은 위치에서 로드된다. 그리고 profile-specific 파일들은 이 파일들이 패키지된 jar파일 안에 있거나 밖에 있거나 상관 없이 non-specific  파일들을 항상 오버라이딩 한다. 


일단 내가 이 테스트를 통해서 알고 싶었던 것은 이부분이었다. active 한 프로파일이 아예 기존 properties 파일을 교체하는지 아니면 기존 파일에 있는 값들을 유지 하되 중복되는 값들은 override 하는지 알고 싶었다. 결과는 기존값을 유지하고 중복값은 override  한다였다.


프로젝트를 여러군데 배포하다 보면 배포 환경에 따라서 값들이 변경되는 값들 있다. 편하게 관리를 하려면 profile을 나누는게 확실히 도움이 되는데 항상 우선순위 때문에 헷갈렸다. 물론 properties 파일이 외부에 있는 경우, commandline 으로 들어오는 경우등 여러 가지 케이스가 더 있긴 할테지만 우선 이것으로 내 궁금증은 풀렸다.


프로젝트 소스 : https://github.com/blusky10/study_spring



728x90
반응형
반응형

백기선님의 Spring Boot 레퍼런스 동영상을 보다 보니 레퍼런스도 한번 쭉~~ 살펴볼 필요가 있겠다 싶은 생각이 들었다. 그럼 뭘 볼까? 라고 생각 하다가 Spring Security 를 보기로 결정 했다. 기간을 가지고 보는것은 아니고 그냥 처음부터 차근차근 정리 하면서 읽어보자라고 생각을 했다. 그런데 보다보니 약간 이상한게 있었다.



위 내용은 레퍼런스에 나오는 5.2장 HttpSecurity 부분이다. 이상하다고 생각한 부분은 "However ~~~~~" 단락이다. 대체 뭘 어떻게 한다는 의미인지 알수가 없었다. 대체 커스텀 로그인 페이지를 만들때 뭘 조심하라는건지. Url이 RESTFul 한데 뭘 어쩌라는건지. 그리고 정보 유출을 막기위해 Spring security 를 사용하는것이 뭐가 분명하지 않다는것인지 이해가 가지 않았다. 커스텀 로그인 페이지를 만들면 spring security가 해줄수 있는 영역이 분명치 않다는 의미인지.. 그리고 마지막에 "For example" 이 있는데 왜 정작 그 다음에는 예제에 대한 내용은 없는 건지 알수가 없었다. 


그래서 내용이 궁금하기도 해서 spring security github issue 페이지에 issue를 등록했다. 



이슈 링크: https://github.com/spring-projects/spring-security/issues/5242#event-1584275921


그랬더니 issue가 처리 되었다고 메일이 왔다. 그래서 뭐가 어떻게 되었는지 확인을 해보았다. 일단 현재 spring security 5.0.4 Release의 reference는 변경사항이 없다. 그래서 git 을 확인해봤다. 뭐 이런 저런 내용이 있었다. 



처음에는 내용을 보니 첫 문단에 뭘 drop했다는 건지는 잘 모르겠다. 그런데 다시 넣었고 적절한 방법은 "For example" 을 지우는 거라고 코멘트가 써있었다.

그리고 그 다음에 또 다른 내용이 있었다.

내용을 살펴보면 이렇다. 

그 단락은 Spring Security가 왜 예전의 default URL 과 parameter에서 새로운 것으로 변경했는지 설명하기 위해서 작성한 것이다. 그러데 XML과 Java Config가 동일해서 지금은 더이상 적절하지 않다.

그래서 결론적으로 

Restore Example Default URLs and Parameters List to Remove Default URLs and Parameters Commentary

그 단락을 삭제했다.


그런데 삭제 했다는데 Reference 에는 아직 존재를 한다. 그래서 다시 Spring Security Github 를 뒤졌다. 


Commit 로그가 저렇게 있었다. 그래서 세부 내역을 확인해 보았다.



히스토리를 보니 저렇게 그 부분이 삭제되어 있다. 아마도 다음 레퍼런스에 반영이 되려나 보다. 레퍼런스를 읽다 보니 이런 일도 생기는구나 라는 생각을 했다. 앞으로 차근차근 더 읽어보면서 또 의문나는것이 있으면 자주 이용을 해야겠다.



728x90
반응형
반응형

테스트 케이스를 작성하다가 좀 헷갈리는게 있었다. @Mock, @MockBean 차이가 뭐지??? 쓰긴 하고 있는데 알고 써야 하지 않을까라는 의문이 들었다. 그래서 찾아봤다.

 

먼저 Mock 객체를 선언할 때에는 2가지 방법이 있다.

 

1. 첫번째 : mock() 을 이용해서 선언

 

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
 
    @InjectMocks
    private UserService userService;
    UserRepository userRepository = mock(UserRepository.class);
 
    @Test
    public void findByEmail_test(){
        when(userRepository.findByEmail("test@test.com")).thenReturn(Optional.of(new User()));
        userService.findByEmail("test@test.com");
        verify(userRepository).findByEmail("test@test.com");
    }
}
cs

 

2. 두번째 : @Mock 을 이용해서 선언

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
 
    @InjectMocks
    private UserService userService;
 
    @Mock
    private UserRepository userRepository;
 
    @Test
    public void findByEmail_test(){
        when(userRepository.findByEmail("test@test.com")).thenReturn(Optional.of(new User()));
        userService.findByEmail("test@test.com");
 
        verify(userRepository).findByEmail("test@test.com");
    }
}
cs

 

이 2개의 테스트 케이스는 동일하게 동작한다. 

 

그리고 선언한 Mock 객체를 테스트를 실행하는 동안 사용할수 있게 하기 위한 작업이 있다. 바로 위에서 보이는 @RunWith(MockitoJUnitRunner.class) 가 바로 그 역할을 해준다.  또다른 방법으로는 아래와 같은 메소드를 테스트케이스 실행 전에 실행 시키면 된다. 

 

 

1
2
3
4
@Before 
public void initMocks() {
    MockitoAnnotations.initMocks(this);
}
cs

 

여태껏 아무생각없이 둘다 @RunWith랑 initMocks랑 둘다 썼는데 둘다 쓸 필요가 없었다. ㅡㅡ;

 

그럼 @MockBean 은 뭐지??

 

@MockBean 은 Mock 과는 달리 org.springframework.boot.test.mock.mockito 패키지 하위에 존재한다. 즉 spring-boot-test에서 제공하는 어노테이션이다. Mockito 의 Mock 객체들을 Spring의 ApplicationContext에 넣어준다. 그리고 동일한 타입의 Bean  이 존재할 경우 MockBean으로 교체해준다.

 

그럼 언제 @Mock을 쓰고 언제 @MockBean 을 써야 하나??

 

결론적으로 Spring Boot Container가 필요하고 Bean이 container 에 존재 해야 한다면 @MockBean 을 쓰면 되고 아니라면 @Mock 을 쓰면 된다. 그런데 개인적으로는 그걸 잘 판단을 못하겠다.

 

1. MockitoJUnitRunner 를 이용해서 Mock을 사용한 Testcase.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(MockitoJUnitRunner.class)
public class RegisterControllerMockTest {
 
    @InjectMocks
    private RegisterController registerController;
 
    private ModelAndView model;
 
    @Before
    public void setup(){
        model = new ModelAndView();
    }
 
    @Test
    public void showRegistrationPage_test() throws Exception {
 
        registerController.showRegistrationPage(model, new User());
        Assert.assertEquals("register", model.getViewName());
    }
}
 
cs

 

2. SpringRunner와 @WebMvcTest를 사용해서 @MockBean 을 사용한 Testcase

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(SpringRunner.class)
@WebMvcTest(value = RegisterController.class, secure = false)
public class RegisterControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockBean
    private UserService userService;
 
    @MockBean
    private EmailService emailService;
 
    @Test
    public void showRegistrationPage_test() throws Exception {
 
        mockMvc.perform(MockMvcRequestBuilders.get("/register"))
                .andExpect(status().isOk())
                .andExpect(view().name("register"))
                .andDo(print());
    }
}
cs

분명 이 2개의 테스트 케이스 는 결과는 같지만 실제 내부에서 동작하는 부분은 다르다. 2번만 spring applicaiton context 가 올라간다. 그래서 부득이하게 @MockBean 으로 UserService 와 EmailService를 넣어줘야 했다. 1번은 Controller 객체에 대해서 Mock 을 만들어서 그거에 대해서만 테스트를 한다.(뭐라고 표현을 해야할지 잘 모르겠다.)

 

이렇게 같은 컨트롤러에 대한 테스트 이지만 작성하기에 따라서 여러가지 방법으로 테스트 케이스를 만들 수 있다. 아직은 미숙하지만 좀더 많이 작성하다 보면 방법도 많이 알게 되고 실제 구현 코드들의 모습도 많이 나아질거라는 생각이 든다.

 

참고 : https://stackoverflow.com/questions/44200720/difference-between-mock-mockbean-and-mockito-mock

728x90
반응형
반응형

https://spring.io/guides/tutorials/spring-boot-oauth2/


위 사이트에 가면 Spring boot 를 이용해서 Oauth를 이용해서 Login 을 할 수 있는 샘플을 만들어볼 수 있다. 그래서 나도 해봤는데.. 그게 삽질의 시작이었다...

Tutorial 자체는 그렇게 어렵지 않게 따라 할 수 있다. 따라하기가 어렵다면 Git에서 소스를 내려 받아서 해볼 수도 있다.


이제 이 Tutorial 을 진행하기 위해서 Facebook Developer 사이트에서 앱을 등록을 해야 한다. 그래야 Client Id 하고 Client  Secret을 받을 수 있다.


https://developers.facebook.com


위 사이트에 들어 가면 본인의 Facebook 계정으로 앱을 등록할 수 있다.


위 화면에서와 같이 앱ID 와 앱 시크릿 코드 를 받을 수 있는데 이게 바로 Client Id 와 Client Secret으로 사용된다.

그리고 redirect URI를 등록하면 준비는 끝난다. (끝난건줄 알았다...)


이제 샘플을 실행 시켜봤다.


로그인 까지 멀쩡하게 됐는데 error가 딱 나온다.


아니 왜????? 대체 왜 에러가 나오는 거지??? 분명 할것 다 한것 같은데..



로그를 보니 분명 access_token 까지는 잘 가져 왔다. 그런데 https://graph.facebook.com/me 라는 url을 호출 할때 400 error 가 났다. Http request 400 Error 는 그냥 Bad Request(요청이 잘못됐다)라는 의미는 아니다.


400(잘못된 요청): 서버가 요청의 구문을 인식하지 못했다

(출처 : https://ko.wikipedia.org/wiki/HTTP_%EC%83%81%ED%83%9C_%EC%BD%94%EB%93%9C#4xx_(%EC%9A%94%EC%B2%AD_%EC%98%A4%EB%A5%98)


한마디로 요청을 하는 syntax 가 뭔가 잘못됐다는 의미이다.


똑같은 요청을 Postman 으로 보내봤다.


리턴된 메세지에 appsecret_proof argument가 있어야 된다고 나온다.. 이게 뭐지??? client_secret 말고 뭐가 또있나???

appsecret_proof로 그래프 API 호출 인증

그래프 API는 클라이언트 또는 클라이언트를 대신하여 서버에서 호출할 수 있습니다. 서버의 호출은 appsecret_proof라고 하는 매개변수를 추가하여 앱의 보안을 개선할 수 있습니다.

액세스 토큰은 이동 가능합니다. 클라이언트에서 Facebook의 SDK에 의해 생성된 액세스 토큰을 취하여 서버에 보낸 다음, 해당 서버에서 클라이언트를 대신하여 호출할 수 있습니다. 사용자 컴퓨터의 악성 소프트웨어 또는 메시지 가로채기(man in the middle) 공격에서 액세스 토큰을 훔칠 수도 있습니다. 그런 다음 클라이언트나 서버가 아닌 완전히 다른 시스템에서 이 액세스 토큰을 사용하여 스팸을 생성하고 데이터를 훔칠 수 있습니다.

서버의 모든 API 호출에 appsecret_proof 매개변수를 추가하고 모든 호출에 대해 인증서를 요청하도록 설정하여 이를 방지할 수 있습니다. 이렇게 하면 악의적인 개발자가 자신의 서버에서 다른 개발자의 액세스 토큰으로 API를 호출할 수 없게 됩니다. Facebook PHP SDK를 사용하고 있다면 appsecret_proof 매개변수는 자동으로 추가되어 있습니다.

(출처 : https://developers.facebook.com/docs/graph-api/securing-requests?locale=ko_KR)


그런 이유로 access_token 이외에 appsecret_proof를 같이 보내야 한다는 거다.



그런데 이런 설명도 있다. 그래서 저기 보이는  Require App Secret(앱시크릿 코드요청) 에 대한 설정을 안하면 appsecret_proof를 추가하지 않아도 된다는 이야기이다. 그래서 저 설정을 No로 설정하면 된다.


이렇게 해서 Facebook 로그인에 대한 샘플이 제대로 작동하는 것을 확인 할 수 있었다. 

간단하게 끝날줄 알았느데 설정 하나 때문에 온갖 고생을 했다. 


728x90
반응형
반응형

ClassPathResource resource = new ClassPathResource(resourcePath);

File file = resource.getFile();

 

위와 같이 코드를 작성해서 로컬에서 실행했을 때에 아무 문제 없이 동작을 하던 것이 war File 배포한 후에 동작을 시켰을 때에 에러가 발생을 했다.

 

class path resource [img/header.jpg] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/home/xxxxadmin/lib/xxxxxxxxx.war!/WEB-INF/classes!/img/header.jpg

java.io.FileNotFoundException: class path resource [img/header.jpg] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/home/xxxxadmin/lib/xxxxxxxxx.war!/WEB-INF/classes!/img/header.jpg

at org.springframework.util.ResourceUtils.getFile(ResourceUtils.java:215)

at org.springframework.core.io.AbstractFileResolvingResource.getFile(AbstractFileResolvingResource.java:52)

 

분명히 존재하는데 찾지를 못하는 거지???

 

https://sonegy.wordpress.com/2015/07/23/spring-boot-executable-jar%EC%97%90%EC%84%9C-file-resource-%EC%B2%98%EB%A6%AC/

 

문제는 소스코드에 있는 resource.getFile() 메소드 때문이었다.

 

war 파일이나 IDE로 application run as로 실행하였다면 실제 resource 파일인 file:// 프로토콜을 쓰기 때문에 File객체를 생성해 줄 수 있지만, executable jar로 실행 했다면 FileNotFoundException이 발생 하게 됩니다.


그래서 아래와 같이 변경해주면 해결이 된다.


InputStream inputStream = classPathResource.getInputStream();

File tempImage = File.createTempFile("temp", ".jpg");

try {

    FileUtils.copyInputStreamToFile(inputStream, tempImage );

} finally {

    IOUtils.closeQuietly(inputStream);

}


728x90
반응형

+ Recent posts