๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ‘ฉ‍๐Ÿ’ป TECH

[TECH] ์„œ๋ฒ„ API ์„ฑ๋Šฅ๊ฐœ์„ ๊ธฐ(2)

by may_yy 2023. 10. 31.

[ํ‹ฐ๋Œ] ํ‹ฐ๋Œ ์„œ๋ฒ„ API ์„ฑ๋Šฅ๊ฐœ์„ ๊ธฐ(2)

์•ˆ๋…•ํ•˜์„ธ์š” ํ‹ฐํ”Œ์˜ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž ๊น€์œ ์ •์ž…๋‹ˆ๋‹ค :)

์ €๋ฒˆ ๊ธ€์— ์ด์–ด์„œ ์˜ค๋Š˜์€ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ํ‹ฐ๋Œ ์•ฑ์—์„œ ๋ฐœ์ƒํ•˜๋Š” N+1 ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์—ฌ ์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ‘ฉ‍๐Ÿ’ป ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ

1. ngrinder์„ ๋จผ์ € ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

 

java -Djava.io.tmpdir=/Users/kimyujeong/.ngrinder/lib -jar ngrinder-controller-3.5.8.war —port=8300

 

 

2. ๋ถ€ํ•˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

 

import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
// import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPRequestControl
import org.ngrinder.http.HTTPResponse
import org.ngrinder.http.cookie.Cookie
import org.ngrinder.http.cookie.CookieManager

import groovy.json.JsonSlurper
import java.util.Random
import groovy.json.JsonOutput

/**
* A simple example using the HTTP plugin that shows the retrieval of a single page via HTTP.
*
* This script is automatically generated by ngrinder.
*
* @author admin
*/
@RunWith(GrinderRunner)
class TestRunner {
	public static GTest test1 // accessToken ์–ป๊ธฐ
	public static GTest test2 // 
	public static HTTPRequest request
	public static Map<String, String> headers = [:]
	public static Map<String, Object> params = [:]
	public static List<Cookie> cookies = []
	

	@BeforeProcess
	public static void beforeProcess() {
		HTTPRequestControl.setConnectionTimeout(300000)
		test1 = new GTest(1, "POST /login/extraInfo/{id} with accountId")
		test2 = new GTest(2, "GET /participate/challenge/list with token")
		
		request = new HTTPRequest()
		grinder.logger.info("before process.")
	}

	@BeforeThread
	public void beforeThread() {
		test1.record(this, "test1")
		test2.record(this, "test2")
		
		grinder.statistics.delayReports = true
		grinder.logger.info("before thread.")
	}
	

	@Before
	public void before() {
		request.setHeaders(headers)
		CookieManager.addCookies(cookies)
		grinder.logger.info("before. init headers and cookies")
	}

	private String accessToken
	
	
	@Test
	public void test1() {
		List<Integer> idList = [...]
		Random random = new Random()
		int randomIndex = random.nextInt(idList.size())
		int id = idList[randomIndex]
	
		def slurper = new JsonSlurper()
		def toJSON = { slurper.parseText(it) }

		HTTPResponse response = request.POST("http://{ip์ฃผ์†Œ}/login/extraInfo/" + id)

		def body = response.getBody(toJSON);
		
		accessToken = body.result.accessToken

		if (response.statusCode == 301 || response.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
		} else {
			assertThat(response.statusCode, is(200))
		}
	}
	

	@Test
	public void test2() {
		headers["X-ACCESS-TOKEN"] = accessToken
		request.setHeaders(headers)
		
		HTTPResponse response = request.GET("http://{ip์ฃผ์†Œ}/participate/challenge/list")

		if (response.statusCode == 301 || response.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
		} else {
			assertThat(response.statusCode, is(200))
		}
	}
}

 

groovy์–ธ์–ด๋Š” ์ž๋ฐ” ์–ธ์–ด์™€ ๋น„์Šทํ•ด์„œ ๊ตฌ๊ธ€๋งํ•˜๋ฉด์„œ ์ž‘์„ฑํ•˜๋ฉด ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

 

ํ•„์ž๋Š” ํ™ˆํ™”๋ฉด์—์„œ (1)'์‚ฌ์šฉ์ž๊ฐ€ ์ฐธ์—ฌํ•˜๊ณ  ์žˆ๋Š” ์ฑŒ๋ฆฐ์ง€ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ' API์— ๋Œ€ํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

QA๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ํ™ˆํ™”๋ฉด์˜ (1) api์—์„œ ์ง€์—ฐ์ด ๋ฐœ์ƒํ•œ ๊ฒƒ์„ ํ™•์ธํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ, ์‚ฌ์šฉ์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด ์ฟผ๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•ด๋ณด๊ณ , ์ด์ „๊ณผ ๋น„๊ตํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

 

๋จผ์ €, ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ์— ๋Œ€ํ•ด์„œ ๊ฐ„๋‹คํžˆ ์„ค๋ช…ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์‚ฌ์šฉ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด, '/login/extraInfo/{id} ' ์—”๋“œํฌ์ธํŠธ๋กœ JWT ํ† ํฐ์„ ๋ฐ›์•„์™€์„œ accessToken ์ „์—ญ๋ณ€์ˆ˜์— ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. 

์ด ๊ฐ’์„ ํ†ตํ•ด ์•ž์œผ๋กœ '/participate/challenge/list'์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ฐธ์—ฌํ•œ ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.. 

 

test1์˜ idList๋Š” ์‚ฌ์šฉ์ž์˜ ๊ณ ์œ  id๊ฐ’์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. ํ‹ฐ๋Œ์—์„œ ์‚ฌ์šฉํ•˜๋Š” '/login/extraInfo/{id}' URL์€ ์นด์นด์˜ค ๋กœ๊ทธ์ธ์„ ๋งˆ์นœ ์‚ฌ์šฉ์ž๊ฐ€ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” URL์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์šฉ์ž์˜ ๊ณ ์œ  id๊ฐ’์ด ํ•„์š”ํ•˜๋ฉฐ, ์ด๋ฅผ idList์— ์ผ์ผํžˆ ์ €์žฅํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด 50๋ช…์˜ ์‚ฌ์šฉ์ž์˜ ๊ฐ’์„ ์ €์žฅํ•˜์˜€๋Š”๋ฐ, ์‚ฌ์šฉ์ž๊ฐ€ ๋” ๋งŽ์•„์ง„๋‹ค๋ฉด ์ด ๋ฐฉ๋ฒ•์€ ์ ์ ˆํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 


๐Ÿ‘ฉ‍๐Ÿ’ป N+1 ๋ฌธ์ œ๋ž€?

JPA๋ฅผ ์‚ฌ์šฉํ•ด ๋ดค๋‹ค๋ฉด, N+1๋ฌธ์ œ์— ๋Œ€ํ•ด ๋“ค์–ด๋ดค์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ x๋Œ€์ผ ์—ฐ๊ด€๊ด€๊ณ„์—์„œ ์ฆ‰์‹œ ๋กœ๋”ฉ์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ํ•ญ์ƒ ํ…Œ์ด๋ธ”์—์„œ ์กฐ์ธ์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ํŠœ๋‹์ด ์–ด๋ ค์›Œ์ง‘๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ์ง€์—ฐ ๋กœ๋”ฉ์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ๊ฐ’์ด ์ฆ‰์‹œ ๋กœ๋”ฉ์ด๊ธฐ ๋•Œ๋ฌธ์—, ํ•„์š”์— ๋”ฐ๋ผ ์ง€์—ฐ ๋กœ๋”ฉ์œผ๋กœ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class ParticipateChallenge {

	...
    
    	@ManyToOne(fetch = FetchType.LAZY)
    	@JoinColumn(name = "account_id")
    	private Account account;
    
	@ManyToOne(fetch = FetchType.LAZY)
    	@JoinColumn(name = "challenge_id")
    	private Challenge challenge;
    
    	...
    
   }

 

 

์‚ฌ์šฉ์ž๊ฐ€ ์ฐธ์—ฌํ•˜๊ณ  ์žˆ๋Š” ์ฑŒ๋ฆฐ์ง€ ๋ฆฌ์ŠคํŠธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

public List<ParticipateChallengeRes> getParticipateChallengeListByAccountId(CustomUserDetails customUserDetails) {
        Account account = accountRepository.findByEmailAndStatus(customUserDetails.getEmail(), Status.VALID)
                .orElseThrow(() -> new CustomException(CustomExceptionStatus.ACCOUNT_NOT_FOUND));

        List<ParticipateChallenge> list = participateChallengeRepository.findByAccountId(account.getId());
        List<ParticipateChallengeRes> collect = list.stream().map(pc -> new ParticipateChallengeRes(pc.getChallenge().getId(), pc.getChallenge().getTitle()))
                .collect(Collectors.toList());
        return collect;
    }

 

ParticipateChallenge ๊ฐ์ฒด๋ฅผ ์กฐํšŒํ•  ๋•Œ, Challenge ๊ฐ์ฒด๋Š” Proxy ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ Challenge์˜ ์ œ๋ชฉ์„ ๊ฐ€์ ธ์˜ค๋Š” pc.getChallenge().getTitle() ์‹œ์ ์—์„œ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

 

์ง€์—ฐ ๋กœ๋”ฉ(FetchType.LAZY)๋Š” ๋กœ๋”ฉ๋˜๋Š” ์‹œ์ ์— LAZY ๋กœ๋”ฉ ์„ค์ •์ด ๋˜์–ด์žˆ๋Š” Challenge ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ”„๋ก์‹œ ๊ฐ์ฒด๋กœ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

์ดํ›„์— ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์‹œ์ ์— ์ดˆ๊ธฐํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง€๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ‘๋‹ˆ๋‹ค.

- getChallenge()์œผ๋กœ Challenge๋ฅผ ์กฐํšŒํ•˜๋ฉด ํ”„๋ก์‹œ ๊ฐ์ฒด๊ฐ€ ์กฐํšŒ๋œ๋‹ค.

- getChallenge().getTitle()์œผ๋กœ ์ฑŒ๋ฆฐ์ง€์˜ ํ•„๋“œ์— ์ ‘๊ทผํ•  ๋•Œ ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ„๋‹ค.

 

 

์ฆ‰, ParticipateChallenge๋ฅผ ์กฐํšŒํ•  ๋•Œ๋Š” ๋จผ์ € Proxy๋กœ ๋œ Challenge ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Challenge์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ ์‹œ์ ์— ์ดˆ๊ธฐํ™”๋˜๋ฉฐ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์ด๋กœ ์ธํ•ด, ์‚ฌ์šฉ์ž๊ฐ€ ์ฐธ์—ฌํ•˜๊ณ  ์žˆ๋Š” ์ฑŒ๋ฆฐ์ง€์˜ ๊ฐœ์ˆ˜์— ๋”ฐ๋ผ ์ด 3๊ฐœ์—์„œ 5๊ฐœ๊นŒ์ง€์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

 

ParticipateChallenge ์ž…์žฅ์—์„œ Challenge๋Š” ์ง€์—ฐ๋กœ๋”ฉ์œผ๋กœ ํ”„๋ก์‹œ ๊ฐ์ฒด๋กœ ์กฐํšŒ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋ฌธ์ œ๋Š” API ์ŠคํŽ™์—์„œ Challenge ํ”„๋ก์‹œ๋ฅผ ์ดˆ๊ธฐํ™”ํ•  ๋•Œ๋งˆ๋‹ค, ์กฐํšŒ๋œ ParticipateChallenge๋งŒํผ ์ถ”๊ฐ€์ ์ธ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋ฐ”๋กœ N+1 ๋ฌธ์ œ์ด๋ฉฐ ์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์„์ˆ˜๋ก ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ๋ฏธ์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ‘ฉ‍๐Ÿ’ป ํ•ด๊ฒฐ๋ฐฉ๋ฒ•

๋‹ค์Œ๊ณผ ๊ฐ™์ด @Query๋ฅผ ํ†ตํ•ด fetch join์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๊ฒฐํ•ด ๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

@Query(value = "select p from ParticipateChallenge p join fetch p.challenge where p.account = :account")

    List<ParticipateChallenge> findAllFetchJoin(Account account);

์œ„ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ•œ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ParticipateChallenge์™€ Challenge๋ฅผ ํ•จ๊ป˜ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 


๐Ÿ‘ฉ‍๐Ÿ’ป ์„ฑ๋Šฅ ์ธก์ •

๊ทธ๋ ‡๋‹ค๋ฉด ์–ผ๋งˆ๋งŒํผ์˜ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์ด๋ค„๋‚ผ ์ˆ˜ ์žˆ์„๊นŒ์š”?

์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์šด์˜์„œ๋ฒ„์— ์ฟผ๋ฆฌ ๊ฐœ์„  ์ „๊ณผ ํ›„๋ฅผ ๋น„๊ตํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Agent๋Š” 1, Vuser per aget๋Š” 50์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

#JPA
์ฟผ๋ฆฌ ๊ฐœ์„  ์ „
#์„ฑ๋Šฅ๊ฐœ์„ 
์ฟผ๋ฆฌ ๊ฐœ์„  ํ›„

TPS(Transaction Per Second)๋Š” ์„œ๋ฒ„๊ฐ€ ์ดˆ๋‹น ์ฒ˜๋ฆฌ ํ•  ์ˆ˜ ์žˆ๋Š” ์š”์ฒญ์˜ ๊ฐœ์ˆ˜๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. 

์ฟผ๋ฆฌ ๊ฐœ์„  ์ „๊ณผ ํ›„๋ฅผ ๋น„๊ตํ–ˆ์„ ๋•Œ 88์—์„œ 95๋กœ, ์•ฝ 8% ์ƒ์Šนํ–ˆ์Šต๋‹ˆ๋‹ค.

 


๐Ÿ‘ฉ‍๐Ÿ’ป ๋งˆ์น˜๋ฉฐ

์„œ๋ฒ„ ์„ฑ๋Šฅ๊ฐœ์„ ๊ธฐ(2)์— ๋Œ€ํ•œ ๊ธ€์„ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!
 
์ด ๊ธ€์„ ํ†ตํ•ด API ์ฟผ๋ฆฌ์˜ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๊ณ  N+1 ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•จ์œผ๋กœ์จ ์„ฑ๋Šฅ ๊ฐœ์„ ์— ์‹ค๋ฌด์ ์ธ ๋Šฅ๋ ฅ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ์˜๋ฏธ์žˆ๋Š” ๊ฒฝํ—˜์ด์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋‹ค์Œ ๊ธ€์—๋„ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ, ํ‹ฐ๋Œ์—์„œ ๊ณ ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„์„ ํ’€์–ด๋‚ด๋Š” ๊ธ€๋กœ ์ฐพ์•„์˜ค๊ฒ ์Šต๋‹ˆ๋‹ค.


๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค ๐Ÿ˜„

 

โฌ‡๏ธ ์ง€๊ธˆ ๊ตฌ๊ธ€ ํ”Œ๋ ˆ์ด ์Šคํ† ์–ด์—์„œ ํ‹ฐ๋Œ ๋‹ค์šด ๋ฐ›๊ธฐ

https://play.google.com/store/apps/details?id=com.team7.tikkle&hl=ko-KR

๐Ÿ“ฉ Contact : uuuuujeong00000@naver.com
๐Ÿ“ฒ SNS : https://www.instagram.com/may_u_j__