Skip to content
fortunetossPublic

About

๋ณต ๋‚˜๋ˆ„๋ฏธ๊ฐ€ ๋˜์–ด ๋‚˜๋ฅผ ํ€ด์ฆˆ๋กœ ๊ณต์œ ํ•˜๊ณ , ๋ณต ๋ฐ›์œผ๋ฏธ๊ฐ€ ๋˜์–ด ์นœ๊ตฌ์˜ ๋งˆ์Œ์„ ๋งžํ˜€๋ณด์„ธ์š”

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

ย 

History

129 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation


bokkie logo
Fortunetoss

์ž์‹ ์— ๋Œ€ํ•œ ํ€ด์ฆˆ๋ฅผ ๋‚ด๊ณ  ์นœ๊ตฌ์™€ ๊ณต์œ ํ•˜๋Š” ํ–‰์šด ๋‚˜๋ˆ” ์›น ์„œ๋น„์Šค


Team Bokkie ๐Ÿฐ

"๋ณต ๋‚˜๋ˆ„๋ฏธ๊ฐ€ ๋˜์–ด ๋‚˜๋ฅผ ํ€ด์ฆˆ๋กœ ๊ณต์œ ํ•˜๊ณ , ๋ณต ๋ฐ›์œผ๋ฏธ๊ฐ€ ๋˜์–ด ์นœ๊ตฌ์˜ ๋งˆ์Œ์„ ๋งžํ˜€๋ณด์„ธ์š”."


๐Ÿ‘ฅ ํŒ€ ๊ตฌ์„ฑ

HyungGeun
BE
[MiYeon]
PM
[HyeWon]
FE
[YunA]
FE
[ChaeWon]
DE
[JiWon]
DE

๐Ÿ“† ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

  • ํ”„๋กœ์ ํŠธ๋ช…: Fortunetoss
  • ๊ธฐ๊ฐ„: 2024.12.20 ~ 2025.01.23 (5์ฃผ)
  • ์ฃผ์ œ: ์ž์‹ ์— ๋Œ€ํ•œ ํ€ด์ฆˆ๋ฅผ ๋งŒ๋“ค์–ด ๊ณต์œ ํ•˜๊ณ , ์นœ๊ตฌ๊ฐ€ ํ€ด์ฆˆ๋ฅผ ํ’€๋ฉฐ ๋•๋‹ด๊ณผ ํ–‰์šด์„ ๋‚˜๋ˆ„๋Š” ์„œ๋น„์Šค
  • ๋ฐฐํฌ: Fortunetoss ํ™ˆํŽ˜์ด์ง€ (ํ˜„์žฌ ๋ฐฐํฌ ์ค‘๋‹จ)

๐ŸŽฏ ๋ฐฑ์—”๋“œ ๋ชฉํ‘œ

  • JPA์™€ QueryDSL์„ ์‚ฌ์šฉํ•œ ๋„๋ฉ”์ธ ์ค‘์‹ฌ ์„ค๊ณ„
  • ์—ฐ๊ด€๊ด€๊ณ„์™€ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ณ ๋ คํ•œ ์—”ํ‹ฐํ‹ฐ ์„ค๊ณ„
  • @Valid ๊ธฐ๋ฐ˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ฐ ์ปค์Šคํ…€ Validator ์ ์šฉ
  • OAuth2 / JWT / Refresh Token ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ตฌ์กฐ
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋„์ž…ํ•˜์—ฌ ๊ฐœ๋ฐœ์˜ ์•ˆ์ „์žฅ์น˜ ํ™•๋ณด ๋…ธ๋ ฅ

๐Ÿงฐ ๊ธฐ์ˆ  ์Šคํƒ

๐ŸŒฑ Spring

๐ŸŒฑ Spring

๊ธฐ์ˆ  ๋ฒ„์ „
Java 17
Spring Boot 3.4.0
Spring Security / OAuth2 6.4.1
JPA 3.4.0
QueryDSL 5.0.0

๐Ÿ—„๏ธ Database

DB ๋ฒ„์ „
MySQL 8.0.x
H2 (ํ…Œ์ŠคํŠธ์šฉ) 2.3.x

๐ŸŒ Infra

๊ธฐ์ˆ 
Docker
Docker Compose
AWS EC2 / RDS / S3

๐Ÿ“ธ ์ฃผ์š” ํ™”๋ฉด

๐ŸŽ ๋ณต ๋‚˜๋ˆ„๋ฏธ (ํ€ด์ฆˆ ์ถœ์ œ์ž)



๋กœ๊ทธ์ธ ๋ฉ”์ธ ํŽ˜์ด์ง€



ํ™ˆ ํ™”๋ฉด



๋ฌธ์ œ ์ž‘์„ฑ ๋ฐ ๋•๋‹ด ์ž…๋ ฅ



์ •๋‹ต์žยท์˜ค๋‹ต์ž ํ†ต๊ณ„ ํ™•์ธ


๐Ÿ€ ๋ณต ๋ฐ›์œผ๋ฏธ (ํ€ด์ฆˆ ํ’€์ด์ž)



๋ฌธ์ œ ํ’€์ด ๋ฐ ๋•๋‹ด ํ™•์ธ



๊ฒฐ๊ณผ ๊ณต์œ  ๋ฐ ์‹ ๊ทœ ํ€ด์ฆˆ ์ƒ์„ฑ


๐Ÿงฉ ERD


๐Ÿ’ก ์ฃผ์š” ๊ธฐ๋Šฅ ๋ฐ ์ฝ”๋“œ ์˜ˆ์‹œ

๐Ÿ” ์ธ์ฆ ํ…Œ์ŠคํŠธ (OAuth2 + JWT)

@Test
@DisplayName("์ผ๋ฐ˜๋กœ๊ทธ์ธ_ํ† ํฐ๋ฐœ๊ธ‰์„ฑ๊ณต")
void ๋กœ๊ทธ์ธ_์„ฑ๊ณต() throws Exception {
    JoinDTO joinDTO = new JoinDTO("goorm94@naver.com", "password1234&");
    String loginRequest = objectMapper.writeValueAsString(joinDTO);

    MvcResult result = mockMvc.perform(post("/login")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(loginRequest))
            .andExpect(cookie().exists("refresh"))
            .andExpect(header().exists("access"))
            .andExpect(status().isOk())
            .andReturn();

    String accessToken = result.getResponse().getHeader("access");
    String refreshToken = result.getResponse().getCookie("refresh").getValue();
    
    assertNotNull(accessToken);
    assertNotNull(refreshToken);
    assertEquals(joinDTO.getUsername(), jwtUtil.getUsername(accessToken));
}


๐Ÿง  QueryDSL (๋ณต์ฃผ๋จธ๋‹ˆ ์กฐํšŒ)

/**
 *  Many ๊ธฐ์ค€ ์กฐํšŒ๋กœ ํŽ˜์ด์ง•์ด ๊ฐ€๋Šฅ
 *  LuckyPouch โ†’ Shape, QuestionCustom ์€ ๋ชจ๋‘ ๋‹จ์ผ ์ฐธ์กฐ ๊ด€๊ณ„
 *  fetch join ์„ ์‚ฌ์šฉํ•ด ์—ฐ๊ด€๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ๋กœ๋”ฉ (N+1 ๋ฐฉ์ง€)
 *  ํŽ˜์ด์ง•(offset + limit) ์€ ์ค‘๋ณต ๋ฐ์ดํ„ฐ ๋ปฅํŠ€๊ธฐ ์—†์Œ
 */

public Slice<LuckyPouch> findUsersLuckyPouches(User findUser, Pageable pageable) {
    List<LuckyPouch> luckyPouches = queryFactory.select(luckyPouch)
        .from(luckyPouch)
        .join(luckyPouch.shape, shape).fetchJoin()
        .leftJoin(luckyPouch.questionCustom).fetchJoin()
        .where(luckyPouch.user.eq(findUser))
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize() + 1)
        .orderBy(
            questionCustomIsNotNullDesc(),
            luckyPouch.questionCustom.createdAt.desc()
        )
        .fetch();

    boolean hasNext = luckyPouches.size() > pageable.getPageSize();
    if (hasNext) luckyPouches.remove(pageable.getPageSize());

    return new SliceImpl<>(luckyPouches, pageable, hasNext);
}


๐Ÿšฆ์กฐ๊ฑด ๊ธฐ๋ฐ˜ ์ •๋ ฌ (Conditional Ordering)

private OrderSpecifier<Integer> questionCustomIsNotNullDesc() {
    return new CaseBuilder()
        .when(luckyPouch.questionCustom.isNotNull()).then(1)
        .otherwise(0)
        .desc();
}


๐Ÿ˜๏ธ์„œ๋ธŒ์ฟผ๋ฆฌ ์‚ฌ์šฉ

  public Question findRandomQuestion(User findUser){
        Question findQuestion = queryFactory.select(question)
                .from(question)
                .where(question.id.notIn(
                        queryFactory.select(pouch.question.id)
                                .from(pouch)
                                .join(pouch.user, user)
                                .where(user.eq(findUser))
                ))
                .limit(1)
                .fetchOne();



        return findQuestion;

    }


๐Ÿ“œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ (์ปค์Šคํ…€ ์• ๋„ˆํ…Œ์ด์…˜ + Validator ์ ์šฉ)

๋‹‰๋„ค์ž„ ํ˜•์‹ ์ œ์•ฝ์„ ์œ„ํ•ด @Nickname ์• ๋„ˆํ…Œ์ด์…˜๊ณผ Validator ์ง์ ‘ ์ •์˜
โ†’ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์ž…๋ ฅ ๊ฒ€์ฆ์„ ์™„์ „ํžˆ ๋ถ„๋ฆฌ

//  ์ปค์Šคํ…€ ์• ๋„ˆํ…Œ์ด์…˜ ์ •์˜
@Constraint(validatedBy = NicknameValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Nickname {
  String message() default "๋‹‰๋„ค์ž„์€ 10์ž ์ด๋‚ด, ํŠน์ˆ˜๋ฌธ์žยท๋„์–ด์“ฐ๊ธฐยท์ด๋ชจ์ง€ ๋ถˆ๊ฐ€";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {}; 
}


//  Validator ๊ตฌํ˜„
public class NicknameValidator implements ConstraintValidator<Nickname, String> {
    //         ์กฐ๊ฑด: 10์ž ์ด๋‚ด, ํŠน์ˆ˜๋ฌธ์ž/๋„์–ด์“ฐ๊ธฐ/์ด๋ชจ์ง€ ํฌํ•จ ๊ธˆ์ง€
    private static final String PATTERN = "^[a-zA-Z0-9ใ„ฑ-ใ…Žใ…-ใ…ฃ๊ฐ€-ํžฃ]{1,10}$";
    @Override
    public boolean isValid(String nickname, ConstraintValidatorContext context) {
        return nickname != null && nickname.matches(PATTERN);
    }
}



//  DTO + ์ปจํŠธ๋กค๋Ÿฌ ์ ์šฉ


public class NicknameRequest {

    @NotNull(message = "๋‹‰๋„ค์ž„์€ ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") // ์ถ”๊ฐ€
    @Nickname
    private String nickname;
}


@PostMapping("/users/validate")
public ResponseEntity<ResponseDto<?>> validateUser(@Valid @RequestBody NicknameRequest request) {

    return new ResponseEntity<>(
        new ResponseDto<>("success", "user validate success", null, null, 200),
        HttpStatus.OK);
}


๐ŸŒฑ ๋„๋ฉ”์ธ ์ดˆ๊ธฐํ™” (์ƒ๋ช…์ฃผ๊ธฐ ํ™œ์šฉ)

@Component
@RequiredArgsConstructor
public class ShapeInitializer {
    private final ShapeRepository shapeRepository;

    @PostConstruct
    public void init() {
        if (shapeRepository.count() == 0) {
            List<String> shapes = List.of("A", "B", "C", "D", "E", "F", "G", "H");
            shapes.forEach(s -> shapeRepository.save(Shape.builder().domain(s).build()));
            System.out.println("Shape ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ");
        }
    }
}


๐Ÿงน ํšŒ์›ํƒˆํ‡ด ๋กœ์ง (์—ฐ๊ด€๊ด€๊ณ„ ์ฐธ์กฐ ๋ฐ ์ƒ๋ช…์ฃผ๊ธฐ ๊ณ ๋ ค)

@Transactional
public void deleteUser() {
    User currentUser = getCurrentUser();

    pouchRepository.deleteByUserId(currentUser.getId());

    luckyPouchRepository.findByUserId(currentUser.getId()).forEach(pouch -> {
        if (pouch.getQuestionCustom() != null) {
            luckyPouchRepository.delete(pouch);
            answerRepository.deleteByQuestionCustomId(pouch.getQuestionCustom().getId());
            questionCustomRepository.deleteById(pouch.getQuestionCustom().getId());
        }
    });

    luckyPouchRepository.deleteByUserId(currentUser.getId());
    userRepository.delete(currentUser);
}


๐Ÿ’ญ ํ”„๋กœ์ ํŠธ ํšŒ๊ณ 

๐Ÿ™‹๐Ÿปโ€โ™‚๏ธ ์ „ํ˜•๊ทผ (BE)

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋Š” ํŒ€ ๋‹จ์œ„์—์„œ ๋„๋ฉ”์ธ ์ค‘์‹ฌ ์„ค๊ณ„(JPA + QueryDSL) ๋ฅผ ์ ๊ทน์ ์œผ๋กœ ์ ์šฉํ•ด ๋ณด์•˜๋‹ค.
๋‹จ์ˆœ CRUD ์ค‘์‹ฌ์˜ ๋ฐฑ์—”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ, ์—”ํ‹ฐํ‹ฐ ๊ฐ„ ์—ฐ๊ด€๊ด€๊ณ„์™€ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ณ ๋ฏผํ•˜๋ฉฐ ์„ค๊ณ„ํ–ˆ๋‹ค๋Š” ์ ์ด ๊ฐ€์žฅ ํฐ ์ฐจ์ด์˜€๋‹ค.

  • QueryDSL๋กœ ๋ณต์žกํ•œ ์กฐ๊ฑด ๊ฒ€์ƒ‰๊ณผ ํŽ˜์ด์ง• ๋กœ์ง์„ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋ฉฐ, SQL๊ณผ ORM ์‚ฌ์ด์˜ ๊ท ํ˜•์„ ์ฒด๊ฐํ–ˆ๋‹ค.
  • OAuth2 + JWT ์ธ์ฆ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถ•ํ•˜๋ฉด์„œ ํ† ํฐ์˜ ๋ฐœ๊ธ‰ยท๊ฐฑ์‹ ยท๋งŒ๋ฃŒ ํ๋ฆ„์„ ์ฝ”๋“œ๋กœ ๋ช…ํ™•ํžˆ ๋‹ค๋ค˜๋‹ค.
  • ์ปค์Šคํ…€ @Valid ๊ฒ€์ฆ๊ธฐ๋ฅผ ๊ตฌํ˜„ํ•ด ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์„ ์„œ๋น„์Šค ๋กœ์ง๊ณผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ์  ์‚ฌ๊ณ ๋ฅผ ์ตํ˜”๋‹ค.
  • ์ƒ๋ช…์ฃผ๊ธฐ(@PostConstruct) ๋ฅผ ํ™œ์šฉํ•ด ๋„๋ฉ”์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋ฉฐ Spring Lifecycle์„ ์ดํ•ดํ–ˆ๋‹ค.

๋‹ค์–‘ํ•œ ์ง๊ตฐ์˜ ํŒ€์›๋“ค๊ณผ ํ˜‘์—…ํ•˜๋ฉฐ ์†Œํ†ตํ•˜๋Š” ๊ณผ์ •์ด ์ฆ๊ฑฐ์› ๊ณ , ๊ฐ์ž์˜ ์—ญํ• ์—์„œ ๋‚˜์˜จ ๋‹ค์–‘ํ•œ ์˜๊ฒฌ์„ ๋“ค์„ ์ˆ˜ ์žˆ์–ด ์œ ์ตํ•œ ๊ฒฝํ—˜์ด์—ˆ๋‹ค.

๐Ÿ’ฌ โ€œ5์ฃผ๊ฐ„์˜ ์งง์€ ํ˜‘์—…์ด์—ˆ์ง€๋งŒ, ๊ฐœ๋ฐœ ๊ธฐ์ˆ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํ˜‘์—…๊ณผ ์†Œํ†ต ๋Šฅ๋ ฅ๊นŒ์ง€ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ๋˜ ๋œป๊นŠ์€ ์‹œ๊ฐ„์ด์—ˆ๋‹ค.โ€


โš™ ์ฐธ๊ณ  ์ž๋ฃŒ

ยฉ 2025 Team Bokkie โ€” Fortunetoss

About

๋ณต ๋‚˜๋ˆ„๋ฏธ๊ฐ€ ๋˜์–ด ๋‚˜๋ฅผ ํ€ด์ฆˆ๋กœ ๊ณต์œ ํ•˜๊ณ , ๋ณต ๋ฐ›์œผ๋ฏธ๊ฐ€ ๋˜์–ด ์นœ๊ตฌ์˜ ๋งˆ์Œ์„ ๋งžํ˜€๋ณด์„ธ์š”

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages