[์†๋‹ฅ์†๋‹ฅ] ๐Ÿ•ฐ ํƒ€์ž„๋จธ์‹  ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ(feat.ํด๋ฝ)

ํ—Œ์น˜ยท2022๋…„ 8์›” 7์ผ
4

์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค

๋ชฉ๋ก ๋ณด๊ธฐ
21/30

ํ•ด๋‹น ๊ธ€์€ ์†๋‹ฅ์†๋‹ฅ ๊ธฐ์ˆ ๋ธ”๋กœ๊ทธ์— ์ž‘์„ฑ๋œ ๊ธ€๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

์†๋‹ฅ์†๋‹ฅ ๋งํฌ

0. Intro

@Entity
@EntityListeners(AuditingEntityListener.class)
public class AuthCode {

    private static final long VALID_MINUTE = 5L;

    @CreatedDate
    private LocalDateTime createdAt;

    //...

    public void verifyTime() {
        LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_MINUTE);
        if (LocalDateTime.now().isAfter(expireTime)) {
            throw new InvalidAuthCodeException();
        }
    }
}

์ธ์ฆ์ฝ”๋“œ๊ฐ€ ์ƒ์„ฑ์‹œ์ ์—์„œ 5๋ถ„์ด ์ง€๋‚˜๋ฉด ๋งŒ๋ฃŒ๋˜๋Š” ๋กœ์ง์„ ์งœ๋˜ ๋„์ค‘์ด์—ˆ๋‹ค.

๊ตฌํ˜„ ํ›„ ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์งœ๋ ค๊ณ  ๋ณด๋‹ˆ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๋‹ค.

LocalDateTime.now() ๋Š” ์‹œ์Šคํ…œ์˜ ์ง€๊ธˆ ์‹œ๊ฐ„ ์„ ๋ฆฌํ„ดํ•˜๋Š”๋ฐ
์–ด๋–ป๊ฒŒ ํŠน์ • ์‹œ๊ฐ„์œผ๋กœ๋ถ€ํ„ฐ ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์งค ์ˆ˜ ์žˆ์ง€? (์–ด๋–ป๊ฒŒ ํƒ€์ž„๋จธ์‹ ์„ ํƒ€์ง€??)
์ฆ‰, ์‹œ๊ฐ„ ๋ณ€์ˆ˜๋ฅผ ํ…Œ์ŠคํŠธ์—์„œ ์ž„์˜๋กœ ์ง€์ •ํ•ด ์“ธ ์ˆ˜ ์—†์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ ํ˜„์žฌ์‹œ๊ฐ์„ ์ž„์˜์ง€์ • ํ›„ ๋ชจํ‚นํ•ด ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ•ด๋ดค๋‹ค.


1. ํ˜„์žฌ ์‹œ๊ฐ์„ ๋ชจํ‚นํ•˜๋Š” ๋ฒ•

์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์—ˆ๋‹ค.

1. Clock ์„ ๋นˆ ๋“ฑ๋กํ•ด ๋ชจํ‚นํ•˜๊ธฐ
2. LocalDateTime ์„ static mock์œผ๋กœ ๋ชจํ‚นํ•˜๊ธฐ
3. LocalDateTime.now() ๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” ์ƒˆ ํƒ€์ž„๋จธ์‹  ํด๋ž˜์Šค ์ƒ์„ฑํ•ด ๋ชจํ‚นํ•˜๊ธฐ
4. AuthCode ์ƒ์„ฑ์ž์—์„œ LocalDateTime ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ฐ›๊ณ , ๊ทธ ์•ˆ์—์„œ LocalDateTime/mock์„ ๋„ฃ๊ธฐ(์ „๋žตํŒจํ„ด)

์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•๋“ค ์ค‘ ๋‚˜๋Š” 1๋ฒˆ,

Clock ์„ ๋นˆ ๋“ฑ๋กํ•ด ๋ชจํ‚นํ•˜๊ธฐ ๋ฅผ ํƒํ–ˆ๋‹ค.

์™œ๋ƒํ•˜๋ฉด

2๋ฒˆ static mock์€ ์•ˆํ‹ฐํŒจํ„ด์ด๊ณ , ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์ž‘์„ฑ ๋ฐฉ์‹์ด ๋ณต์žกํ•˜๊ณ , try๋ฌธ์œผ๋กœ ๊ฐ์‹ธ์•ผ ํ•œ๋‹ค. ์ผ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ˜ธํ™˜๋˜์ง€ ์•Š๋Š”๋‹ค.
3๋ฒˆ์€ ์ƒˆ๋กœ์šด ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“œ๋Š” ๊ณผ์ •์ด ๋ฒˆ๊ฑฐ๋กญ๊ณ , ๊ธฐ์กด LocalDateTime์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด ์•„๋‹ˆ์–ด์„œ ํด๋ž˜์Šค ์ดํ•ด๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
4๋ฒˆ์€ ์–ด๋””๊นŒ์ง€ ํ•ด๋‹น ๊ฐ์ฒด๋ฅผ ๋„ฃ์–ด์ค„์ง€ ๋ฒ”์œ„๋ฅผ ๋Š์–ด์ฃผ๊ธฐ ์–ด๋ ค์› ๋‹ค.(Controller โ†’serviceโ†’domainโ€ฆ)


2. ์™œ Clock์ธ๊ฐ€?

1) LocalDateTime์€ ๋ชจํ‚นํ•  ์ˆ˜ ์—†๋‹ค

public final class LocalDateTime
        implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable {

    public static LocalDateTime now() {
        return now(Clock.systemDefaultZone());
    }

    public static LocalDateTime now(ZoneId zone) {
        return now(Clock.system(zone));
    }

    public static LocalDateTime now(Clock clock) {
        Objects.requireNonNull(clock, "clock");
        final Instant now = clock.instant();  // called once
        ZoneOffset offset = clock.getZone().getRules().getOffset(now);
        return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset);
    }
    //... of() ๋“ฑ ์ด์™ธ ๋ฉ”์†Œ๋“œ
}

LocalDateTime.now()๋Š” ๋‹ค์Œ ๋ฐฉ์‹์œผ๋กœ ์งœ์—ฌ์žˆ๋‹ค.

๋ณด๋‹ค์‹œํ”ผ now()๋Š” static ๋ฉ”์†Œ๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์—
LocalDateTime์„ ๊ฐ์ฒด๋กœ ๋“ฑ๋กํ•ด ์‚ฌ์šฉํ•˜๋”๋ผ๋„ ๋ฉ”์†Œ๋“œ๋ฅผ ๊บผ๋‚ด์“ธ ์ˆ˜ ์—†๋‹คโ€ฆใ… ใ… 

๊ทธ๋Ÿฐ๋ฐ ์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ์ ์ด ์žˆ๋‹ค.

public static LocalDateTime now(Clock clock) {
	Objects.requireNonNull(clock, "clock");
    final Instant now = clock.instant();  // called once
    ZoneOffset offset = clock.getZone().getRules().getOffset(now);
    return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset);
}

ํ‰์†Œ์—๋Š” LocalDateTime.now()๋กœ ํ˜„์žฌ ์‹œ๊ฐ„์„ ๊ฐ€์ ธ์˜ค์ง€๋งŒ,

์‚ฌ์‹ค LocalDateTime.now(clock) ํ˜•ํƒœ๋กœ, Clock๊ฐ์ฒด๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์•„ ์‹œ๊ฐ„์„ ๊ฐ€์ ธ์˜ค๋Š” ํ˜•ํƒœ์˜€๋‹ค!

2) Clock์ด ๋ญ์ง€?

๊ทธ๋ ‡๋‹ค๋ฉด ์ด Clock์€ ๋ญ˜๊นŒ?

์ •ํ™•ํžˆ ๋งํ•˜๋ฉด

final Instant now = clock.instant();  // called once

์—์„œ ์‚ฌ์šฉ๋˜๋Š” clock.instant()๋Š” ๋ญ˜๊นŒ?

public abstract class Clock {
        //-----------------------------------------------------------------------
        /**
         * Gets the current instant of the clock.
         *<p>
        * This returns an instant representing the current instant as defined by the clock.
         *
         *@returnthe current instant from this clock, not null
         *@throwsDateTimeExceptionif the instant cannot be obtained, not thrown by most implementations
         */
        public abstract Instant instant();
        //...
}

Clock ๊ฐ์ฒด์˜ ํ˜„์žฌ instant๋ฅผ ๋ฆฌํ„ดํ•ด์ฃผ๋Š” ์ถ”์ƒ ๋ฉ”์†Œ๋“œ์ด๋‹ค.

3) Clock์„ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•˜์ž

์ฆ‰, LocalDateTime์˜ ๋ฉ”์†Œ๋“œ๋“ค์€ static ๋ฉ”์†Œ๋“œ์ง€๋งŒ,
clock ๊ฐ์ฒด๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ
clock์„ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ด ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค!

3. ์–ด๋–ป๊ฒŒ ๋ชจํ‚นํ•˜์ง€?

1) ํ˜„์žฌ ์‹œ๊ฐ„(now)์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ถ”์ถœ

public class AuthCode {

    public void verifyTime(LocalDateTime now) { // now๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ด๋™
        LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_MINUTE);
        if (now.isAfter(expireTime)) {
            throw new InvalidAuthCodeException();
        }
    }
}

authcode.verifyTime()๋ฉ”์†Œ๋“œ์—์„œ

ํ˜„์žฌ ์‹œ๊ฐ„(now)์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ถ”์ถœํ–ˆ๋‹ค.

2) clock์„ Bean ๋“ฑ๋ก

import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TimeConfig {

    @Bean
    public Clock clock() {
        return Clock.systemDefaultZone();
    }
}

๋‹ค์Œ, clock์„ @Configuration ์œผ๋กœ ๋นˆ ๋“ฑ๋กํ–ˆ๋‹ค.

public abstract class Clock {        
    public static Clock systemDefaultZone() {
        return new SystemClock(ZoneId.systemDefault());
    }
}

์ด๋•Œ Clock.systemDefaultZone() ์„ ํ™œ์šฉํ–ˆ๋‹ค.

clockํด๋ž˜์Šค์˜ ํ˜„์žฌ์‹œ๊ฐ„ clock ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค.

3) AuthService ์—์„œ Bean ์ฃผ์ž…

๊ทธ๋ฆฌ๊ณ  AuthService์—์„œ clock์„ ๋นˆ์œผ๋กœ ์ฃผ์ž…๋ฐ›์€ ํ›„

AuthCode ๊ฐ์ฒด์— clock์„ ์ฃผ์ž…ํ•œ๋‹ค.

@Service
public class AuthService {

    private final AuthCodeRepository authCodeRepository;
    // ...
    private final Clock clock;

    public AuthService(//...,
                       Clock clock) {
        this.authCodeRepository = authCodeRepository;
        this.clock = clock;
    }

    public void verifyAuthCode(VerificationRequest verificationRequest) {

        AuthCode authCode = authCodeRepository.findBySerialNumber(serialNumber)
                .orElseThrow(SerialNumberNotFoundException::new);
        //...
        LocalDateTime now = LocalDateTime.now(clock);
        authCode.verifyTime(now);
    }
    // ...
}

4. ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ•˜์ง€?

1) AuthCode(๋„๋ฉ”์ธ ๊ณ„์ธต)

AuthCode์˜ ๊ฒฝ์šฐ ๋“ฑ๋ก๋œ clock ๋นˆ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ๋„ ๋‹ค์Œ ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

@SpringBootTest
class AuthCodeTest {
    @DisplayName("์ธ์ฆ์ฝ”๋“œ ์ƒ์„ฑ์‹œ๊ฐ„์œผ๋กœ๋ถ€ํ„ฐ 5๋ถ„์ด ์ง€๋‚˜๋ฉด ์ธ์ฆ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค")
    @Test
    void verifyTime_Exception_Time() {
        AuthCode authCode = AuthCode.builder()
                .code("ABCDEF")
                .serialNumber("21f46568bf6002c23843d198af30bb2bc8123695bd3d12ce86e0fc35bc5d3279")
                .createdAt(LocalDateTime.parse("2007-12-03T10:15:30"))
                .build();

        assertThatThrownBy(() -> authCode.verifyTime(LocalDateTime.parse("2007-12-03T10:20:31")))
                .isInstanceOf(InvalidAuthCodeException.class);
    }
}

2) AuthService(์„œ๋น„์Šค ๊ณ„์ธต)

AuthService์˜ ๊ฒฝ์šฐ clock ๋นˆ์„ ์ด์šฉํ•ด ๋‹ค์Œ ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

class AuthServiceTest extends IntegrationTest {

    private static final Clock FUTURE_CLOCK = Clock.fixed(Instant.parse("3333-08-22T10:00:00Z"), ZoneOffset.UTC);
    @Autowired
    private AuthService authService;
    @Autowired
    private AuthCodeRepository authCodeRepository;

    @SpyBean
    private Clock clock;

    @DisplayName("์ธ์ฆ๋ฒˆํ˜ธ ๋งŒ๋ฃŒ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ")
    @Test
    void verifyAuthCode_Exception_Expired() {
        AuthCode authCode = //~
        authCodeRepository.save(authCode);

        // clock.instant() ๋ฆฌํ„ด๊ฐ’์„ ์ž„์˜๋กœ ์ง€์ •ํ•ด ๋ฏธ๋ž˜๋กœ ํ˜„์žฌ ์‹œ๊ฐ„์„ ๋ฐ”๊พผ๋‹ค!
        doReturn(Instant.now(FUTURE_CLOCK))
                .when(clock)
                .instant();

        VerificationRequest verificationRequest = new VerificationRequest("test@gmail.com", "ABCDEF");
        assertThatThrownBy(() -> authService.verifyAuthCode(verificationRequest))
                .isInstanceOf(InvalidAuthCodeException.class);
    }
}

๐Ÿ“š์ฐธ๊ณ ์ž๋ฃŒ

(Java) ํƒ€์ž„๋จธ์‹ ์„ ํƒ€๊ณ  ์‹œ๊ฐ„ ์—ฌํ–‰ ๋– ๋‚˜๊ธฐ
ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„  LocalDate.now()๋ฅผ ์“ฐ์ง€๋ง์ž.
How can I mock java.time.LocalDate.now()

profile
๐ŸŒฑ ํ•จ๊ป˜ ์ž๋ผ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค ๐Ÿš€ rerub0831@gmail.com

3๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2022๋…„ 8์›” 10์ผ

์™€ ํ›Œ๋ฅญํ•˜๋„ค์š”!!

1๊ฐœ์˜ ๋‹ต๊ธ€
comment-user-thumbnail
2022๋…„ 9์›” 20์ผ

I think this is the first time I am looking at these codings and the great work of by an individual. In case if you need help in promoting your work consider hire social media manager london. As they are very professional in doing digital marketing

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ