springboot기초(4)-멤버를 추가해보자

2024. 10. 3. 16:31개발자 과정/Kotlin

가위바위보 결과를 저장하는 DB도 연결했고, 로직도 고도화를 했다. 

하지만, 여러 사용자의 히스토리를 관리하고 싶다면? 

그것을 실현하기 위해선 멤버를 추가해야 한다. 

이번엔 멤버에 대한 밑바탕을 만들어 보자. 

이제 점점 심해로 간다..!!

 

패키지 구조

우선 패키지 구조를 재정립하자. 우리는 이제 멤버에 대한 서비스도 핸들링을 해야한다. 

앞으로 추가될 기능들을 고려해서라도 다음과 같이 바꿔줘야 한다. 

domain이라는 상위 디렉토리가 생기고, 그 안에 memberrps를 나누었다

 

이 다음은 우리가 기존에 해왔던 대로 객체를 만들어주면 된다. 

 

구현

 

MemberController

@RestController
@RequestMapping("/member")
class MemberController(
    private val memberService: MemberService
) {

    @PostMapping("/signup")
    fun signUp(@RequestBody signRequest: SignRequest): ResponseEntity<MemberResponse>{
        return ResponseEntity.status(HttpStatus.CREATED).body(memberService.signUp(signRequest))
    }

    @PostMapping("/signin")
    fun signIn(@RequestBody signRequest: SignRequest):ResponseEntity<MemberResponse>{
        return ResponseEntity.ok().body(memberService.signIn(signRequest))
    }
}

간단한 회원 가입로그인 기능을 추가한다. 

@RequestBody 를 사용함에 주의하자. 그간 썼던 @RequestParam과는 다르다!

전체적인 흐름을 보면 알겠지만, @RequestBody 를 통해 우리는 dto를 전달받으려 함을 알 수 있다. 

 

dto

data class MemberResponse (
    val nickName:String,
    val id:Long?
){
    companion object{
        fun signUpFrom(memberData: MemberData):MemberResponse{
            return MemberResponse(nickName = memberData.nickname,null)
        }

        fun signInFrom(memberData: MemberData):MemberResponse{
            return MemberResponse(nickName = memberData.nickname, id = memberData.id)
        }
    }
}
data class SignRequest(
    val nickname:String,
    val password:String
)

회원가입, 로그인을 하는데 필요한 간단한 dto이다. 편의상 memberResponse에서 signupsignin의 반환을 수행한다.

 

entity

@Entity
@Table(name="memberdata")
class MemberData (
    @Column(name = "nickname")
    val nickname:String,

    @Column(name="password")
    val password:String,

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id:Long?=null
)

단순한 닉네임비밀번호를 가진다.

 

repository

interface MemberDataRepository:JpaRepository<MemberData,Long> {
    fun findByNickname(nickname:String):MemberData
}

우리는 로그인을 해야하기 때문에 닉네임을 찾는 과정이 필요하다.

 

service

@Service
class MemberService(
    private val memberDataRepository: MemberDataRepository
) {
    fun signUp(signRequest: SignRequest):MemberResponse {
        return MemberResponse.signUpFrom(
            memberDataRepository.save(
                MemberData(
                    signRequest.nickname,
                    signRequest.password
                )
            )
        )
    }

    fun signIn(signRequest: SignRequest):MemberResponse {
        return MemberResponse.signInFrom(
            memberDataRepository.findByNickname(signRequest.nickname)
                .also { check(signRequest.password == it.password) })
    }
}

단순하게 구현한 회원가입, 로그인 로직이다. 원래는 이보다 더욱 복잡한 로직을 가져야 하지만, 

일단 대략적인 배경정도만 만들어놓은거라 생각하면 된다. 앞으로 이 부분을 고도화 시키는 과정도 필요할 것이다.

 

RpsData

@Entity
@Table(name="rpsdata")
class RpsData(
    @Column(name = "memberid")
    val memberId:Long,

    @Column(name = "result")
    val result: String,

    @Column(name = "user_select")
    val userSelect:String,

    @Column(name = "com_select")
    val comSelect:String,

    @Column(name="user_score")
    val userScore:Int,

    @Column(name="com_score")
    val comScore:Int,

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id:Long?=null
)

rpsdata도 변화가 있다. 바로, 유저별로 히스토리를 가져야 하기에 이에 맞춰 memberId 컬럼이 추가된 것이다.

이대로 한 번 실행해주자. 

 

application.yml

  jpa:
#    hibernate:
#      ddl-auto: update

다시 종료 후 ddl-auto옵션은 이제 꺼주자. 엔티티의 설정을 보고 테이블을 업데이트 시키는 옵션인데, 

앞으로 개발하는데 있어 이제부턴 꺼주는게 좋다. 왜냐면 h2db에 직접 들어가 설정을 할 것이기 때문이다.

 

DB설정

인텔리제이의 이렇게 생긴 아이콘을 클릭하고,

데이터 소스에서 h2를 선택한다.

다음과 같이 뜨면 연결타입을 Embedded로 설정하고 우리가 로컬로 저장하고 있는 데이터 베이스의 경로를 입력 후 연결한다. 로컬에 영속적인 저장을 하는 h2모드embedded mode라고 한다.

 

그렇다면 이렇게 뜰 것이다.

 

그 후 MEMBERID를 선택해 열 수정에 들어가 다음과 같이 Fk설정을 하면 된다. 

원래 spring에선 객체간 "연관관계 매핑" 을 통해 엔티티관계설정을 해주는 것이 일반적이다.

엔티티 구현 코드에서 객체간의 연관관계를 설정하면, 그 관계에 맞춰 엔티티를 핸들링 할 수 있는 것인데, 

나는 이런 방법을 그렇게 선호하진 않는다. 왜냐면 객체간 강결합으로 인해 기초설계에서 조금만 엇나가도 관계가 꼬여버리고, 그로인한 프로그램의 유연성이 줄어드는 것을 좋아하지 않기 때문이다.

그래서 내가 하는 방법이 엔티티간의 연관관계는 설정해놓지 않고 그저 id를 통해 추적만 가능하게 만들어 놓은 다음, 

연관관계를 통해 얻을 수 있는 이점 (연관관계에 따른 자동삭제, 자동수정)DB자체에서 fk를 통해 옵션으로 걸어줌으로 챙겨오는 방식이다. 이렇게 하면 객체간의 결합도는 낮아지고, 확장성이 늘어나는 이점이 있다.  

물론, 각각의 장단점이 있기에 뭐가 더 좋다라곤 말할 수 없다. 

 

RpsController

@RequestMapping("/rps")
@RestController
class RpsController(
    private val rpsService: RpsService
) {

    @PostMapping("/play")
    fun play(@RequestParam memberId:Long, @RequestParam userRps: RpsSelect)
        : ResponseEntity<RpsResultResponse> {
        return ResponseEntity.status(HttpStatus.CREATED).body(rpsService.play(memberId,userRps))
    }

    @GetMapping("/history")
    fun history(@RequestParam memberId: Long):ResponseEntity<List<RpsResultResponse>>{
        return ResponseEntity.ok().body(rpsService.history(memberId))
    }
}

@RequestParam으로 memberId를 추가한다. 

 

RpsDataRepository

interface RpsDataRepository : JpaRepository<RpsData,Long>{
    fun save(rpsData: RpsData): RpsData
    fun findByMemberId(memberID:Long):List<RpsData>
}

그리고 memberId를 통해 데이터를 찾아오는 jpa함수를 추가한다. 

 

RpsService

fun history(memberId: Long):List<RpsResultResponse>{
    return rpsDataRepository.findByMemberId(memberId)
        .map { RpsResultResponse.from(it) }
}

히스토리 로직은 모든 데이터를 찾아오는 것이 아닌 해당memberId에 소속된 데이터만을 뽑아오도록 바뀌었다.

play함수에선 뭐가 바뀌어야 할지 생각해 보자, 간단하다!

 

여기까지 하고 swagger를 실행하고 테스트 해보자. 

회원가입을 하고, 로그인을 하여 유저의 고유 id값을 받아온 후 게임을 플레이하면 유저에 따른 히스토리가 저장되는 것을 확인 할 수 있을 것이다. 

그리고, db로 들어가 유저 정보를 삭제시키면, 해당 유저의 히스토리도 같이 지워지는 것을 확인 할 수 있을 것이다.

 

다만, 마음에 걸리는 것이 있을 것이다. 이런식으로 유저id를 넘겨주면, 다른 사람도 해당 유저로 rps로직에 접근하기 너무 쉽지 않을까? 앞으로는 이런 부분들을 보완하기 위한 방안들도 얘기하고자 한다.