항해99/개발일지

20220203 개발일지 #개인과제 정리

paran21 2022. 2. 3. 21:24

어제 새벽에 테스트 코드 구현은 했지만 테스트 코드로서의 기능은 제대로 못하는 것 같다.

오전에 username 중복 검사 부분을 프론트에서 넘겨주는 부분과 별개로 회원가입 서비스부분에 따로 구현을 해주고 다시 테스트 코드를 완성하였다.

오후에 심화 4주차 강의를 시작했고 팀과제를 마무리하였다.

이번주에는 자바나 알고리즘의 거의 손을 못댔는데, 앞으로도 틈틈히 잘 볼 수 있을지 걱정이 된다.

심화 강의에 디버깅을 하는 부분이 있었는데 앞으로 코드를 짤 때 중간중간 확인하면서 작업하기 좋을 것 같다.

 

개인과제 프로젝트 : 로그인 기능이 포함된 게시판 구현하기

github: https://github.com/paran22/springprac2

전체적으로 구현해야할 기능이 많았다.

기능 구현하면서 회원가입 유효성 검사같은 경우에는 프론트에서 구현을 해야할지 서버에서 해야할지 고민이 많았고

프론트에서 어떤 방식으로 보여주어야 보기편할지 생각해본 부분도 많았다.

 

#전체 구조

전체적으로 게시판과 관련된 Board, 댓글과 관련된 Comment, 회원가입 및 로그인 부분인 User로 나뉘고 각각 Controller, Service, Repository, Entity와 Dto를 구현하였다.

페이지 로딩은 기본적으로 Thymeleaf를 사용하여 GET요청으로 접속 시 해당 뷰를 먼저 뿌려주고 그 뒤에 ajax로 데이터를 보내는 방식으로 구현하였다.

각 페이지마다 어떤 데이터를 보내고 어떤 값을 response할지, 그에 따른 Entity구조나 메소드를 어떻게 할지 많이 고민하였다.

#Controller

페이지 로딩과 관련된 controller는 모두 HomeController에 구현하였다.

데이터만 내려주는 BoardController와 CommentController는 RestController이다.

게시글은 등록할 때 로그인 정보를 가져와서 userId와 username을 함께 저장해주었다.

//게시글 등록하기
//아이디 함께 저장하기
@PostMapping("/write")
public Board createBoard(@RequestBody BoardDto boardDto,
                         @AuthenticationPrincipal UserDetailsImpl userDetails) {
    Long userId = userDetails.getUser().getId();
    String username = userDetails.getUser().getUsername();
    Board board = boardService.createdBoard(boardDto, userId, username);
    return board;
}

//게시글 조회하기
@GetMapping("/boards/detail/{id}")
public Board getBoard(@PathVariable Long id){
    Board board = boardService.getBoard(id);
    return board;
}

댓글도 입력할 때 로그인 정보로 userId, username을 함께 저장하였다.

그리고 댓글을 수정할 때는 우선 수정할 댓글을 먼저 조회하고(1), 수정 후 업데이트된 정보를 다시 save(2)하는 방식으로 구현하였다.

또한, 댓글은 작성자 본인만 수정/삭제 버튼이 보이도록 하기 위해 해당 페이지에 로그인 정보(loginId)를 보내주는 컨트롤러를 만들었다.

//댓글 입력하기
    @PostMapping("/boards/detail")
    public Comment createComment(@RequestBody CommentDto commentDto,
                                 @AuthenticationPrincipal UserDetailsImpl userDetails) {
        Long userId = userDetails.getUser().getId();
        String username = userDetails.getUser().getUsername();
        Comment comment = commentService.createdComment(commentDto, userId, username);
        return comment;
    }

    //댓글 수정하기
    //1. 댓글 조회하기
    @GetMapping("/boards/comment/{id}")
    public Comment getComment(@PathVariable Long id) {
        Comment comment = commentService.getComment(id);
        return comment;
    }

    //2. 댓글 수정하기
    @PutMapping("/boards/comment/{id}")
    public Long updateComment(@PathVariable Long id, @RequestBody CommentDto commentDto) {
        Comment comment = commentService.updateComment(id, commentDto);
        return comment.getId();
    }

    //댓글 삭제하기
    @DeleteMapping("/boards/comment/{id}")
    public Long deleteComment(@PathVariable Long id) {
        commentService.deleteComment(id);
        return id;
    }

    //댓글 수정, 삭제 권한 부여를 위해 user값 보내기
    @GetMapping("/logininfo")
    public Long getLoginInfo(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        Long loginId = userDetails.getUser().getId();
        return loginId;
    }
}

#Entity

카카오 로그인에서 생기는 kakaoId를 User에 칼럼으로 추가하고, 일반 회원가입시에는 null값이 들어가도록 하였다.

굳이 카카오와 다른 유저를 구분해야할 필요가 없었고, 회원 별로 구분해서 구현해야할 기능이 없었기 때문에 크게 문제될 부분은 없다고 생각했다.

하지만 프로젝트가 커지고 회원 db도 관리가 필요해지면 일괄적으로 관리하기 위해 별도로 분리하는게 나을 수도 있을 것 같다.

public User(String username, String password, String email) {

    this.username = username;
    this.password = password;
    this.email = email;
    this.kakaoId = null;
}

public User(String username, String password, String email, Long kakaoId) {
    this.username = username;
    this.password = password;
    this.email = email;
    this.kakaoId = kakaoId;
}

#Repository

Spring Jpa의 도움을 받아 필요한 메소드들을 추가로 구현하여 사용하였다.

findby*는 Optional로 설정하였고, 프론트에서 회원가입시 username 중복을 확인하기 위해 boolean 값으로 return되는 메소드를 구현하였다.

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);

    Optional<User> findByKakaoId(Long kakaoId);

    //username 중복확인
    boolean existsByUsername(String username);
}

#WebSecurityConfig

Spring security의 기본 속성들은 여기서 정의가 이루어진다.

post를 차단하는 csrf는 disable로 하여 기능별로 따로 허용하지는 않았다.

어차피 post는 모두 로그인을 한 상태에서만 가능하고, 잘못 들어간 경우에는 errorpage로 들어가 로그인or홈페이지로 이동을 선택하게 하였다.

별도로 회원별 권한을 구분하는 기능은 없기 때문에 단순히 로그인 여부에 따라서(인증, authentication) 허용되는 페이지만 별도로 구분하였다.

로그인페이지를 errorpage로 하여 로그인이 필요한 경로로 접근시에는 로그인으로 안내하게 하였고, 로그인 성공 후 url, 로그아웃 후 url 등 필요한 설정들은 바꿔서 사용하였다.

@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
//@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder encodePassword() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) {
// h2-console 사용에 대한 허용 (CSRF, FrameOptions 무시)
        web
                .ignoring()
                .antMatchers("/h2-console/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
//        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

        http.authorizeRequests()
// css 폴더를 login 없이 허용
                .antMatchers("/css/**").permitAll()
// 회원 관리 처리 API 전부를 login 없이 허용
                .antMatchers("/user/**").permitAll()
// /boards/ API 전부를 login 없이 허용
                .antMatchers("/boards/**").permitAll()
                .antMatchers("/errorpage/**").permitAll()
                // 그 외 어떤 요청이든 '인증'
                .anyRequest().authenticated()
                .and()
// [로그인 기능]
                .formLogin()
// 로그인 View 제공 (GET /user/login)
                .loginPage("/errorpage")
// 로그인 처리 (POST /user/login)
                .loginProcessingUrl("/user/login")
// 로그인 처리 후 성공 시 URL
                .defaultSuccessUrl("/")
// 로그인 처리 후 실패 시 URL
                .failureUrl("/user/login?error")
                .permitAll()
                .and()
// [로그아웃 기능]
                .logout()
// 로그아웃 요청 처리 URL
                .logoutUrl("/user/logout")
                .logoutSuccessUrl("/")
                .permitAll()
                .and()
                .exceptionHandling();
    }
}

#Service

회원가입 유효성을 구현하면서, 테스트 코드를 작성하면서 고민이 많았던 부분이다.

처음에 @Valid를 사용하여 유효성 검사를 하고, 에러 메시지 객체를 model로 view에 던져주는 방식으로 구현을 했었는데 비밀번호에 username이 포함되면 안되는 것과 같은 조건들은 단순히 어노테이션만으로는 해결이 안되고 어차피 추가로 구현이 필요했다.

그리고 구현을 하면서 굳이 모든 유효성검사를 위해 데이터를 서버에 보내주고 다시 서비가 데이터를 내려주는게 비효율적이라는 생각이 들었다.

회원가입 페이지가 form + action으로 구현되어 있었고 ajax와 함께 결합해서 유효성 검사를 하는게 쉽지는 않았지만 기본적으로는 프론트에서 모든 검사를 끝내고 회원가입이 이루어지게 하였다.

 

username의 경우 중복검사 후에 readonly속성을 부여하여 검사 후 아이디를 수정하는 것을 막아놓았지만 그래도 가장 중요한 username 중복은 서버에서 한번 더 체크를 하는 것이 맞다고 생각했다.

테스트 코드를 작성하면서 다른 유효성 검사 항목들은 SignupValidation 클래스에 메소드를 만들었고 중복검사는 service에 추가하였다.

중복검사도 SignupValidation에 구현하려고 했는데, 해당 메소드를 static으로 가져와서 그 안에 repository 구현이 되지 않았다.

테스트 코드에서도 에러가 발생하고 유효성 검사의 성격도 다른 것 같아서 2번에 거쳐 검사가 이루어지게 하였다.

@RequiredArgsConstructor
@Service
public class UserService {
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    public void registerUser(SignupRequestDto requestDto) {
        if (SignupValidation.validationSignupInput(requestDto)) {
            String username = requestDto.getUsername();
            if (userRepository.existsByUsername(username)) {
                throw new IllegalArgumentException("중복된 username 입니다.");
            }
            // 패스워드 암호화
            String password = passwordEncoder.encode(requestDto.getPassword());
            String email = requestDto.getEmail();

            User user = new User(username, password, email);
            userRepository.save(user);
        }
    }
    //username 중복확인
    public boolean checkUsername(String username) {
        return userRepository.existsByUsername(username);
    }
}
@AllArgsConstructor
@Component
public class SignupValidation {

    public static boolean validationSignupInput(SignupRequestDto signupRequestDto) {
        String username = signupRequestDto.getUsername();
        String password = signupRequestDto.getPassword();
        String passwordConfirm = signupRequestDto.getPasswordConfirm();
        //username 확인
        if (!username.matches("^[a-zA-Z0-9]{3,15}$")) {
            throw new IllegalArgumentException("username 조건이 맞지 않습니다.");
        }

        //password 확인
        if (password.length() < 4) {
            throw new IllegalArgumentException("비밀번호는 최소 4자 이상입니다.");
        }

        if (password.contains(username)) {
            throw new IllegalArgumentException("비밀번호에 username을 사용할 수 없습니다.");
        }

        //passwordConfirm 확인
        if (!password.equals(passwordConfirm)) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        return true;
    }
}

#html

detail 페이지가 가장 구현이 어려웠다.

댓글을 조회할 때 해당 게시글의 id로 조회를 해야하는데 처음에 댓글id, userid와 혼동하였다.

 

그리고 로그인한 사용자면 수정/삭제 버튼이 보이도록 하였는데, 로그인한 사용자의 id와 해당 댓글의 userid를 비교하는 부분에서 변수를 어떻게 받아야 할지, 어디서 비교를 해서 어떤 값을 차이가 나게 해야 할지 고민이 되었다.

처음에 로그인한 사용자의 userId를 model로 넘기는 방법도 고민했었는데, 이 경우 필요한 모든 뷰 controller에 해당 객체를 추가해주어야 했다.

또, Thymeleaf로 받은 데이터를 변수로 지정하는 방법을 처음에 찾지 못했고(결국 다른 분께 방법을 공유받긴 했다.), 모든 데이터들은 대부분 ajax로 넘기고 있어서 전체적인 틀을 유지하고 싶었다.

ajax로 콜을 하니 loginId가 필요한 페이지마다 ajax를 작성해줘야하는 불편함이 있었다.(사실 이 부분은 js를 따로 코드 분리하면 해결 가능하다)

login과 signup 페이지에서는 이렇게 loginId를 불러서 로그인이 되어 있다면 alert를 뜨게 구현하였다.

//로그인되어 있는 경우
function loginCheck() {
    //숫자인 경우
    if (!isNaN(loginId)) {
        if (window.confirm("이미 로그인이 되어있습니다.")) {
            window.location = "/";
        }
    }
}

로그인 여부에 따라 페이지 뷰가 달라져야 하는 부분이 있었는데(예를 들면 로그인/로그아웃 버튼 등)

처음에는 아에 html파일을 두 개 만들어서 작성했었다.

그러나 이렇게 하면 모든 페이지를 두 개 만들어야 되서 다른 방법을 찾았고, Thymeleaf 문법을 사용해서 구현하였다.

<!--인증하지 않았을때 보이는 화면-->
<div sec:authorize-expr="!isAuthenticated()">
<ul class="nav">
    <li class="nav-item">
        <a class="nav-link" href="/user/login">로그인</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="/user/signup">회원가입</a>
    </li>
</ul>
</div>
<!--인증시 보이는 화면-->
<div sec:authorize-expr="isAuthenticated()">
    <a class="btn btn-primary" href="/write" role="button">글쓰기</a>

    <form id="my_form" method="get" action="/user/logout">
        <a id="logout-text" href="javascript:{}" onclick="document.getElementById('my_form').submit();">로그아웃</a>
    </form>
</div>

<div sec:authorize-expr="isAuthenticated()">
<h1><span sec:authentication="principal.user.username">demo</span>님의</h1>
</div>

#TestCode

유효성 검사는 다음과 같이 Dto의 오류를 포함하여 작성하고 에러메시지가 정상적으로 출력되는지 확인하였다.

@Test
@DisplayName("실패 케이스_username_한글")
void signupFail1() {
    //given
    SignupRequestDto requestDto = new SignupRequestDto(
            "아이디",
            "a0123456789",
            "a0123456789",
            "abc@gmail.net"
    );

    //when
    Exception e = assertThrows(IllegalArgumentException.class, () -> {
        SignupValidation.validationSignupInput(requestDto);
    });

    //then
    assertEquals("username 조건이 맞지 않습니다.", e.getMessage());
}

username 중복검사는 repository를 이용하여 어떤 username이 들어가도 존재하는 경우(즉 중복 username 존재하는 경우)로 설정하고 출력된 에러메시지를 비교하는 방식으로 구현하였다.

@Nested
@DisplayName("username 중복검사")
class DubCheckUsername {

    @InjectMocks
    private UserService userService;

    @Mock
    private UserRepository userRepository;

    @Test
    @DisplayName("중복1")
    void checkUsername1() {
        //given
        SignupRequestDto requestDto = new SignupRequestDto(
                "admin1",
                "a0123456789",
                "a0123456789",
                "abc@gmail.net"
        );

		//어떤 값이 들어가도 항상 해당값이 존재(즉, 중복 username이 존재하는 경우)
        given(userRepository.existsByUsername(any())).willReturn(true);


        // when
        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            userService.registerUser(requestDto);
        });

        // then
        assertEquals("중복된 username 입니다.", exception.getMessage());
    }

원하는 방식의 테스트 코드를 작성하는 것이 어려웠다.

전체적으로 테스트하고자 하는 코드에 대한 이해가 있어야 작성을 할 수 있을 것 같다.

controller를 테스트하는 mvc test도 더 공부해서 구현해보고 싶다.

 

더 공부를 해야하는데 이번 주에 개인과제 프로젝트를 하면서 너무 지쳐서 쉽지 않다.

다음 과제가 너무 어렵지만 않게 나왔으면 좋겠다.

그리고 바로 프론트와 협업하는 프로젝트들이 시작되는데 잘 구현할 수 있을지 걱정된다.

'항해99 > 개발일지' 카테고리의 다른 글

20220206 개발일지 #배달앱 기본 기능 구현  (0) 2022.02.06
20220204 개발일지  (2) 2022.02.06
20220202 개발일지  (0) 2022.02.03
20220131 개발일지 #@Valid  (0) 2022.02.01
20220129 개발일지  (0) 2022.01.29