[wanted] 원티드 프리온보딩 "Today I learned... 10-25"

Hojun Song ·2023년 10월 25일
0
post-thumbnail

 과제를 받은 오늘 아침, 프로젝트를 시작하기 전에 팀원들과 첫 소통을 했다. 이전에 엘리스 트랙에서 만났던 사람들은 대부분 프론트엔드를 선호했고, 백엔드는 누구도 하고 싶어하지 않는 포지션이었다.

 하지만 여기서는 상황이 달랐다. 나는 지금까지는 백엔드를 적극적으로 담당하고, 잘 모르지만 매번 팀장을 맡으며 리딩해왔지만, 여기선 현 팀원들의 지식과 의견을 듣고 나의 부족함을 많이 느꼈다. 무엇을 설명하려고 해도, 정확한지 확신이 없었고, 백엔드에 열정을 쏟아온 팀원들의 시선이 내게 부끄러움을 주기도 했다. 더군다나 Git 컨벤션등을 지키려고 하는 모습에, 지난 프로젝트를 중구난방으로 커밋했던 내 자신이 그려졌다. 난 그리고 완성된 프로젝트로 기준으로 전부 NoSQL로, RDBMS에는 너무 약했다. 개발 이야기와는 번외로 내게 귀가 두 개가 있고 입이 하나인 이유를 다시 한번 새기면서, 항상 경청해야함을 다시 마음에 새기고 블로그에 포스팅하는 마음을 가진다.

궁금증이 가시지 않는 NoSQL이 적합할지, RDBMS를 써야되는지, GPT는 이렇게 분석했다.

RDBMS(관계형 데이터베이스 관리 시스템) vs NoSQL:

서비스의 특성을 고려할 때, RDBMS(관계형 데이터베이스 관리 시스템)와 NoSQL 중 어떤 데이터베이스 시스템을 선택할지 결정할 수 있습니다. 아래는 각 시스템의 특징과 서비스 요구사항에 대한 분석을 통해 어떤 시스템이 더 적합할지 제안합니다:

RDBMS:

  1. 스키마 정의:
    • RDBMS는 미리 정의된 스키마를 기반으로 데이터를 저장하며, 각 필드의 데이터 타입을 정의해야 합니다.
    • 이는 게시물의 구조가 정해져 있고, 이메일 인증, 로그인, 통계 등의 기능에서 일관된 데이터 처리를 요구할 때 유용합니다.
  2. 트랜잭션 처리:
    • RDBMS는 ACID(Atomicity, Consistency, Isolation, Durability) 속성을 지원하여 트랜잭션 처리에 강점이 있습니다.
    • 이는 좋아요나 공유와 같은 동작을 처리할 때 데이터의 일관성을 보장하는데 도움이 됩니다.
  3. 조인 연산:
    • RDBMS는 테이블 간의 관계를 통해 조인 연산을 지원합니다.
    • 이는 통계 데이터를 생성하거나 다양한 SNS 플랫폼 간의 데이터를 관리할 때 유용할 수 있습니다.

NoSQL:

  1. 스케일 아웃:
    • NoSQL 데이터베이스는 높은 트래픽과 데이터의 빠른 증가를 처리하기 위해 스케일 아웃이 가능합니다.
    • 이는 복수의 SNS 플랫폼에서 큰 데이터를 빠르게 처리할 필요가 있을 때 유용할 수 있습니다.
  2. 유연한 스키마:
    • NoSQL은 유연한 스키마를 제공하여, 다양한 데이터 구조를 쉽게 저장하고 관리할 수 있습니다.
    • 이는 다양한 SNS 플랫폼의 데이터 구조를 하나의 시스템에서 처리할 때 유용할 수 있습니다.
  3. 빠른 읽기/쓰기:
    • NoSQL은 빠른 읽기 및 쓰기 성능을 제공하며, 대용량 데이터를 효율적으로 처리할 수 있습니다.

제안:

  • 해당 어플리케이션의 경우, 다양한 SNS 플랫폼에서 가져온 데이터를 유연하게 처리할 필요가 있을 것으로 보입니다. 이를 위해 NoSQL 데이터베이스를 사용하는 것이 더 적합할 수 있습니다. 특히, 각 SNS 플랫폼의 데이터 구조가 서로 다를 수 있으며, 빠른 읽기 및 쓰기 성능이 요구될 것으로 예상됩니다.
  • 또한, 서비스가 확장될 경우를 대비하여, NoSQL의 스케일 아웃 기능을 활용하여 높은 트래픽과 데이터의 빠른 증가를 효율적으로 처리할 수 있을 것입니다.
  • 그러나, 통계와 같은 기능에서는 정확한 데이터 처리와 트랜잭션의 일관성이 중요할 수 있으므로, 이러한 부분을 처리하기 위해 RDBMS를 함께 사용하는 혼합 모델을 고려할 수도 있습니다.

최종적으로는 서비스의 요구사항과 팀의 기술 스택, 그리고 미래의 확장성을 고려하여 더 적합한 데이터베이스 시스템을 선택해야 합니다.

 물론, GPT의 한계로 정확하지 않을 수 있지만, 해당 제안서를 보고 관계가 필요한지, 독립적으로 테이블이 구성되는지 파악할 수 없다는 것은 내게 큰 약점이라는 생각이 들었다. 일단 Nest가 내겐 사실상 처음인 프레임워크이다보니, 이번 업무에서도 유저 인증을 선택했다. 지겨울만도 하지만, 프레임워크가 달라지면 나름 신선하게 다가오기도 해서, 다만, 매번 사용자 인증 관련을 나혼자 개발해왔지만, 이번엔 팀원 한명과 함께 해야하기 때문에 소통하고 소통해야겠다고 다짐했다. 이젠 데이터모델링과 Validate하는 예시를 먼저 검색해보았다.

데이터모델링 및 Validate 예시

NestJS:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { Validate, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@Schema()
export class User extends Document {
    @Prop({ unique: true })
    account: string;  // Username managed as hashtag
    
    @Validate(PasswordConstraint)
    @Prop()
    password: string;
    
    @Prop({ unique: true })
    email: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

@ValidatorConstraint({ name: 'ValidPassword', async: false })
export class PasswordConstraint implements ValidatorConstraintInterface {
    validate(password: string, args: ValidationArguments) {
        if (password.length < 10) return false;

        const commonPasswords = ['123456', 'password', '123456789'];
        if (commonPasswords.includes(password)) return false;

        if (/^\d+$/.test(password)) return false;

        const types = [/\d/.test(password), /[a-zA-Z]/.test(password), /\W/.test(password)];
        if (types.filter(Boolean).length < 2) return false;

        if (/([a-zA-Z\d\s])\1{2,}/.test(password)) return false;

        return true;
    }

    defaultMessage(args: ValidationArguments) {
        return 'Password does not meet complexity requirements!';
    }
}

Spring Boot(Kotlin):

import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.mapping.Document

@Document
data class User(
    @Id val id: String? = null,

    @Indexed(unique = true)
    val account: String,  // Username managed as hashtag

    @ValidPassword
    val password: String,

    @Indexed(unique = true)
    val email: String,
    val oldPassword: String? = null
)


@Constraint(validatedBy = [PasswordValidator::class])
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class ValidPassword(
    val message: String = "Invalid password",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = []
)

class PasswordValidator : ConstraintValidator<ValidPassword, String> {
    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        if (value == null) return false

        if (value.length < 10) return false

        val commonPasswords = listOf("123456", "password", "123456789")
        if (commonPasswords.contains(value)) return false
s
        if (value.all { it.isDigit() }) return false

        val types = listOf(value.any { it.isDigit() }, value.any { it.isLetter() }, value.any { !it.isLetterOrDigit() })
        if (types.count { it } < 2) return false

        if (value.contains(Regex("""(.)\1{2,}"""))) return false

        return true
    }
}

Express에서도 타입스크립트를 사용하면, 저 데코레이터를 쓸 수 있는 것으로 알고 있다. 내게 스프링에서 어노테이션과 Nest의 데코레이터를 봤을 때, 스프링과 NestJS의 철학을 공유하고 있지 않나, 라는 생각을 갖게 해줬다.

지난 프로젝트 에서는 스프링 시큐리티 도입을 할까 하다가, 간단한 인터셉터 방식으로 개발을 진행했다. Express와 FastAPI와는 다르게 미들웨어 개념을 쓰지 않아서 처음으로 회원가입을 구현하는데 애를 먹었던 기억이 났다.

@Configuration
class WebMvcConfig(
    private val jwtTokenInterceptor: JwtTokenInterceptor,
) : WebMvcConfigurer {
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(jwtTokenInterceptor)
            .addPathPatterns("/**")
            .excludePathPatterns(
                "/swagger-ui/**",
                "/v3/**",
                "/api/user/nickname/guest",
                "/api/follow/guest/followings",
                "/api/follow/guest/followers",
                "/api/oauth/kakao/**",
                "/api/comment/guest/**",
                "/api/trip/nickname/**",
                "/api/trip/guest/**",
                "/api/trip/like/user/**",
                "/favicon.ico",
            )
    }
}


@Component
class JwtTokenInterceptor(
    private val jwtService: JwtService,
    @Value("\${admin.ids}") private val adminIdsConfig: String,
) : HandlerInterceptor {

    private val adminIds: List<Long> by lazy {
        adminIdsConfig.split(",").mapNotNull { it.trim().toLongOrNull() }
    }

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        val authorization = request.getHeader("Authorization") ?: throw UnauthorizedException("Authorization 헤더가 없습니다.")

        val token = authorization.removePrefix("Bearer ").trim()
        if (!jwtService.validateToken(token)) {
            throw UnauthorizedException("유효하지 않은 토큰입니다.")
        }

        val memberId: Long = jwtService.getMemberIdFromToken(token)
        request.setAttribute("memberId", memberId)

        if (request.requestURI.contains("/admin/")) {
            if (!adminIds.contains(memberId)) {
                throw ForbiddenException("관리자만 접근 가능합니다.")
            }
        }
        return true
    }
}

그래서, 미들웨어와는 다르게 오히려 로그인이 필요없는 엔드포인트를 모아 excludePathPatterns에 넣어서, 모든 신호에 토큰 여부를 확인하는 방식으로 구현했다.

Nest에서는 주로 Passport.js와 함께 JWT 인증을 구현을 하는 정보를 습득했다. 이 방식에서는 @nestjs/jwt 및 passport-jwt 패키지를 사용하여 JWT 인증을 처리하고, Passport의 전략을 따르는 것으로 많은 검색 결과가 나왔다.

@Module({
  imports: [UserModule, PassportModule, JwtModule.register({
    secret: 'secretKey',
    signOptions: { expiresIn: '60s' },
  }), MongooseModule.forFeature([{ name: "user", schema: UserSchema }])],
  providers: [AuthService, UsersService, LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule { }

Nest의 Module이 내게는 크게 익숙하게 다가오지 않은데, 개발을 하면서 익숙해져야할 포인트처럼 느껴진다.

profile
A web backend developer, let's share information and problem solving!

0개의 댓글