Skip to main content

Spring JPA TransactionTemplate Internals

· 10 min read

회사에서 프론트 개발을 하다가 서버 개발을 제대로 시작한지 1년 반 정도 지났습니다. Spring 관련 인터넷 강의, 공식 문서 외에는 따로 공부하고 있지 않았는데 회사에서 겪은 문제를 기반으로 Spring JPA 내부 코드를 파악했던 경험을 기록해두려고 합니다.

첫번째 문제, add column to vehicle table DDL

최근에 타다 서버를 배포 하던 중에 차량 관련 테이블에 컬럼을 추가하는 DDL 과 차량 위치 추적 데이터 처리에서 deadlock 이 발생했습니다. 타다에서는 드라이버(driver), 운행차량(vehicle), 운행(ride) 등이 foreign key 를 이용해서 참조하고 있습니다. 그리고 일반 정보를 저장하는 데이터베이스와 차량 위치를 추적하는 데이터베이스를 따로 관리하고 있습니다.

class TestController(
private val transactionTemplate: TransactionTemplate,
@Qualifier("trackerDB") private val trackerTransactionTemplate: TransactionTemplate,
) {
private fun transactionInTrackerTransaction() {
trackerTransactionTemplate
.also { it.isolationLevel = IsolationLevel.SERIALIZABLE }
.execute {
val trackerRide = trackerRepository.findById("...")

// No explicit transaction
val acceptedRide = rideRepository.findById("...")

transactionTemplate
.execute {
driverRepository.findById("...")
vehicleRepository.findById("...")
}
}
}
}

타다 차량의 위치를 처리하는 과정은 위 코드와 같이 차량 위치(tracker), 운행정보(ride), 드라이버(driver) 그리고 차량(vehicle) 테이블에 접근합니다. 이 과정은 많은 드라이버의 위치 정보를 처리해야 하므로 빈번하게 호출됩니다. 이러한 상황에서 vehicle 에 column 을 추가하는 DDL 을 실행하면 vehicle, vehicle 과 foreign key 로 연결된 테이블(ride, driver, ...)에 metadata lock 을 잡으려고 합니다. DDL 이 vehicle → ride 로 metadata lock 을 획득하는 과정에서 위 코드에서는 ride → vehicle 순서로 metadata lock 을 잡으려고 하니 deadlock 상태에 빠져버렸습니다. rideRepository.findById 는 데이터를 가져온 후 metadata lock 을 해제해야하는데 trackerTransactionTemplate 범위 내내 lock 을 획득한 채로 있어 deadlock 에 빠졌습니다. 위 문제는 rideRepository.findById 을 transactionTemplate 으로 감싸면 ride 에 대한 metadata lock 이 빠르게 해제되면서 해당 문제는 해결됩니다.

두번째 문제, ReadReplica Transaction in Serializable Transaction

다른 기능을 배포하는 과정에서 could not execute statement [The MySQL server is running with the --read-only option so it cannot execute this statement] 에러가 발생했습니다. 타다에서는 읽기 부하 분산을 위해 ReadReplica 데이터베이스를 사용하며, TransactionTemplate에 isReadReplicaEnabled라는 커스텀 속성을 추가하여 ReadReplica로 라우팅할 수 있도록 구현했습니다.

@RestController
class TestController(
private val transactionTemplate: TransactionTemplate,
) {
@Transactional(isolation = Isolation.SERIALIZABLE)
private fun readReplicaInSerializable() {
// 인증 토큰 확인은 아래와 같이 유저 정보에 접근한다.
transactionTemplate
.also { it.isReadReplicaEnabled = true }
.execute {
userRepository.findById("...")
}
val ride = rideRepository.findById("...")
// update something
rideRepository.save(ride)
}
}

대부분 API 에 토큰에 대한 검증을 하는 과정이 있습니다. 이때 마스터 데이터베이스를 보기 보다는 READ REPLICA 데이터베이스에 접근해서 부하를 분산합니다. 근데 위와 같이 코드를 작성하면 이후 데이터를 업데이트하는 rideRepository.save 에서 문제가 발생합니다. 외부 Transaction이 먼저 시작되었으니 마스터 데이터베이스 연결을 사용하고, 내부 transactionTemplate도 이를 재활용할 것으로 기대했지만 실제로는 다르게 동작했습니다.

두 문제에 대해서 정확한 원인을 파악하기 위해서는 TransactionTemplate 이 어떻게 동작하는지, TransactionTemplate 이 없을 때 Repository 로 데이터 접근할 때 어떻게 동작하는지 그리고 데이터베이스에 연결을 어떻게 맺는지 이해할 필요가 있었습니다.

먼저 TransactionTemplate 동작 이해하기

TransactionTemplate.execute 는 크게 3 step 으로 나눌 수 있습니다.

public <T> T execute(TransactionCallback<T> action) {
// Step 1: Transaction 에 필요한 Context 를 구성하기
TransactionStatus status = this.transactionManager.getTransaction(this);

try {
// Step 2: Context 로 데이터에 접근하기
result = action.doInTransaction(status);
} catch() { /* rollback */ }

// Step 3: Commit 과 리소스 정리
this.transactionManager.commit(status);
return result;
}

Transaction 에 필요한 Context 를 구성하기

public final TransactionStatus getTransaction(TransactionDefinition definition) {
Object transaction = doGetTransaction();
if (isExistingTransaction(transaction)) {
return handleExistingTransaction(definition, transaction, debugEnabled);
}
return startTransaction(definition, transaction, debugEnabled, suspendedResources);
}

AbstractPlatformTransactionManager.getTransaction 에서는 이미 선언한 Transaction 이 있는지, 이미 Transaction 이 진행중이라면 Propagation 정책에 따라 새로운 Transaction 에 필요한 Context 를 만들지 정합니다.

// JpaTransactionManager.doGetTransaction
EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource(obtainEntityManagerFactory());
if (emHolder != null) {
txObject.setEntityManagerHolder(emHolder, false);
}
if (getDataSource() != null) {
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(getDataSource());
txObject.setConnectionHolder(conHolder);
}

여기서 Context(EntityManager, Connection) 는 TransactionSynchronizationManager 를 통해 가져옵니다. TransactionSynchronizationManager 는 Resource 들을 ThreadLocal 에 저장합니다.

새로 시작하는 Transaction 의 경우 저장된 Context 가 없기 때문에 startTransaction 함수에서 새로 만듭니다.

// JpaTransactionManager.java
if (!txObject.hasEntityManagerHolder() || txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
EntityManager newEm = createEntityManagerForTransaction();
txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
}
EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
TransactionSynchronizationManager.bindResource(obtainEntityManagerFactory(), txObject.getEntityManagerHolder());

ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
ConnectionHolder conHolder = new ConnectionHolder(conHandle);
TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
txObject.setConnectionHolder(conHolder);

EntityManager 를 새로 만들고, JdbcConnection 을 TransactionSynchronizationManager 을 통해 ThreadLocal 에 저장합니다.

Context 로 데이터에 접근하기

Context 를 가져와서 Callback 함수를 처리하는데 보통 Repository 로 데이터를 가져오고 씁니다. Repository.findById 함수를 호출하면 Proxy 객체를 통해 TransactionAspectSupport.invokeWithinTransaction 를 호출합니다. 함수명에서 유추할 수 있듯이 시작한 Transaction Context 를 불러와서 데이터에 접근합니다.

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
TransactionManager tm = determineTransactionManager(txAttr);

if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

try {
retVal = invocation.proceedWithInvocation();
} finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
}

createTransactionIfNecessary 에서는 TransactionManager.getTransaction 를 호출하여 TransactionTemplate 동작과 비슷하게 이미 정의한 EntityManager, Connection 를 이용해서 데이터에 접근합니다. 데이터 접근 이후에는 commitTransactionAfterReturning 을 통해 Transaction 상태에 따라 commit 을 진행합니다.

Transaction 없이 Repository.findById 함수를 호출하면 조금 다르게 동작합니다. createTransactionIfNecessary 내부에서 Transaction Context 를 가져오지 못합니다. Query 실행 내부를 따라가다보면 SharedEntityManagerCreator proxy 객체에서 EntityManager 직접 만드는 걸 찾을 수 있습니다.

public abstract class SharedEntityManagerCreator {
private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
EntityManager target = EntityManagerFactoryUtils.doGetTransactionalEntityManager(this.targetFactory, this.properties, this.synchronizedWithTransaction);
// ...
if (target == null) {
target = this.targetFactory.createEntityManager();
isNewEm = true;
}
}
}
}

public abstract class EntityManagerFactoryUtils {
public static EntityManager doGetTransactionalEntityManager(EntityManagerFactory emf, @Nullable Map<?, ?> properties, boolean synchronizedWithTransaction) throws PersistenceException {
EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource(emf);
if (emHolder != null) {
return emHolder.getEntityManager();
}
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return null;
}
if (em == null) {
em = (!CollectionUtils.isEmpty(properties) ? emf.createEntityManager(properties) : emf.createEntityManager());
}
TransactionSynchronizationManager.bindResource(emf, emHolder);
}
}

TransactionSynchronizationManager.isSynchronizationActive 를 통해서 만든 EntityManager 를 ThreadLocal 에 저장할지 말지 판단합니다. Transaction 이 없는 경우는 TransactionSynchronizationManager 에 저장된 Synchronization 이 없을테니 리소스를 저장하지 않고 바로 해제합니다.

참고) [JPA] SimpleJpaRepository의 EntityManager는 어디서 생성될까?

Commit 과 리소스 정리

TransactionManager.commit 에서는 commit, doAfterCommit, Context 정리하는 과정을 거칩니다.

if (status.isNewSynchronization()) {
for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) {
TransactionSynchronizationManager.unbindResource(synchronization);
}
}

if (status.isNewTransaction()) {
doCommit(status); // Actual database commit
}
triggerAfterCommit(status);
cleanupAfterCompletion(status);

이전 섹션에서 언급했듯이, TransactionTemplate 을 시작할 때와 Repository 접근에서 commit 함수를 부릅니다. 그때 모든 Transaction 에서 데이터베이스 commit 을 부르는 건 아니고 isNewTransaction 을 통해서 최상위 Transaction 에서만 commit 을 호출합니다.

private void cleanupAfterCompletion(DefaultTransactionStatus status) {
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.clear();
}
if (status.isNewTransaction()) {
JpaTransactionObject txObject = status.getTransaction();
txObject.getEntityManagerHolder().clear();
// ...
}
}

Context 를 정리하는 과정에서는 TransactionSynchronizationManager 를 통해서 ThreadLocal에 저장되어있는 리소스들을 정리합니다.

예시

transactionTemplate
.also { it.isolationLevel = IsolationLevel.SERIALIZABLE }
.execute { // 1
val driver = driverRepository.findByIdOrNull("DVC40729") // 2
}
  1. TransactionTemplate 의 시작으로 TransactionSynchronizationManager 에 정의된 EntityManager 가 없기 때문에 TransactionManager.startTransaction 를 통해 EntityManager 를 만들고 JdbcConnection 을 맺습니다. 만든 객체는 TransactionSynchronizationManager 를 통해 ThreadLocal 에 저장합니다.
  2. Repository 함수는 Proxy 객체로 만들어져 있어 내부에서 TransactionAspectSupport.invokeWithinTransaction 를 호출하고 TransactionSynchronizationManager 를 통해 ThreadLocal 에 저장된 EntityManager 를 가져와 데이터에 접근합니다.

최근에 마주한 문제들 해결하기

TransactionTemplate 과 Repository 동작을 이해했으니 처음에 언급한 문제들을 차례대로 분석해보겠습니다.

첫번째 문제, add column to vehicle table DDL

class TestController(
private val transactionTemplate: TransactionTemplate,
@Qualifier("trackerDB") private val trackerTransactionTemplate: TransactionTemplate,
) {
private fun transactionInTrackerTransaction() {
trackerTransactionTemplate
.also { it.isolationLevel = IsolationLevel.SERIALIZABLE }
.execute { // 1
val trackerRide = trackerRepository.findById("...") // 2

val acceptedRide = rideRepository.findById("...") // 3

transactionTemplate
.execute { // 4
driverRepository.findById("...")
vehicleRepository.findById("...")
}
} // 5
}
}
  1. TrackerDatabaseConfiguration 에서 주입한 TransactionManager 를 통해 새로운 Transaction 을 시작합니다. TransactionSynchronizationManager 에 정의한 EntityManager 가 없으니 새로 만듭니다.

  2. trackerRepository.findByIdTransactionAspectSupport.invokeWithinTransaction 를 호출하고 1 에서 정의한 EntityManager 를 활용해 데이터를 가져옵니다.

  3. rideRepository.findByIdTransactionAspectSupport.invokeWithinTransaction 를 호출하지만, PrimaryDatabaseConfiguration 의 EntityManagerFactory 를 사용하므로 새로운 EntityManager 를 만듭니다.

    • 이전에 언급했듯이, Transaction 이 없는 Repository 접근은 EntityManager 를 새로 만들어 접근 후 바로 해제합니다.
    • 그런데 Tracker Transaction 내부이기 때문에 TransactionSynchronizationManager 에 저장된 Synchronization 이 존재합니다. 그래서 PrimaryDatabase 에 대한 Transaction 이 아니더라도 ThreadLocal 에 EntityManager 를 저장합니다.
      if (!TransactionSynchronizationManager.isSynchronizationActive()) {
      return null;
      }
      if (em == null) {
      em = (!CollectionUtils.isEmpty(properties) ? emf.createEntityManager(properties) : emf.createEntityManager());
      }
      TransactionSynchronizationManager.bindResource(emf, emHolder);
    • 이후 데이터를 불러오고 나서 commit 하면서 리소스를 해제할 수 있지만 Transaction 이 없기 때문에 commitTransactionAfterReturning 에서 commit 도 부르지 않습니다.
      protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
      // ...
      try {
      retVal = invocation.proceedWithInvocation();
      } finally {
      cleanupTransactionInfo(txInfo);
      }
      commitTransactionAfterReturning(txInfo);
      }
  4. 새로운 PrimaryDatabase 의 Transaction 이 시작합니다. Transaction 을 시작할 때 ThreadLocal 에 저장된, 3번에서 만든 EntityManager 를 재활용 한다고 생각할 수 있지만 재활용하지 않고 새로 만듭니다.

    • 여기서는 이전에 언급하지 않은 suspend / resume 에 대해서 알아야합니다.

    • 새로운 Transaction 을 만들 때 같은 EntityManager 를 접근하게 되면 의도치 않은 객체 변경/flush 를 유발할 수 있습니다. 이미 진행중인 EntityManager 는 suspend 함수를 통해 TransactionSynchronizationManager 에서 제거 후 해당 TransactionObject 에 저장합니다. 새롭게 열리는 Transaction 에서는 새로운 EntityManager 를 만들고 데이터를 접근합니다. 3번에서 만든 Context 를 suspend 합니다.

      // 참고 [What does suspending a transaction mean?](https://stackoverflow.com/questions/33729810/what-does-suspending-a-transaction-mean)
      public final TransactionStatus getTransaction(TransactionDefinition definition) {
      Object transaction = doGetTransaction();
      if (isExistingTransaction(transaction)) {
      return handleExistingTransaction(definition, transaction, debugEnabled);
      }
      SuspendedResourcesHolder suspendedResources = suspend(null);
      return startTransaction(definition, transaction, debugEnabled, suspendedResources);
      }

      private TransactionStatus handleExistingTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled) throws TransactionException {
      // ...
      if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
      SuspendedResourcesHolder suspendedResources = suspend(transaction);
      try {
      return startTransaction(definition, transaction, debugEnabled, suspendedResources);
      }
      }
      }
    • 새로운 Transaction 이 끝날 때 commit 을 호출하고 리소스를 정리하는 과정에서 resume 함수를 호출하고 이전 suspend 했었던 Context 를 복구합니다. 여기서 3번에서 만든 EntityManager 를 다시 ThreadLocal 에 저장합니다.

      private void cleanupAfterCompletion(DefaultTransactionStatus status) {
      // ...
      if (status.getSuspendedResources() != null) {
      resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
      }
      }
  5. trackerTransactionTemplate 를 commit 하면서 기존 TransactionSynchronizationManager 에 있던 resource 들을 모두 정리한다. 이때 3번에서 정의한 Context 까지 같이 정리하면서 ride 테이블의 metadata lock 이 해제됩니다.

두번째 문제, ReadReplica Transaction in Serializable Transaction

@RestController
class TestController(
private val transactionTemplate: TransactionTemplate,
) {
@Transactional(isolation = Isolation.SERIALIZABLE)
private fun readReplicaInSerializable() {
// 인증 토큰 확인은 아래와 같이 유저 정보에 접근한다.
transactionTemplate
.also { it.isReadReplicaEnabled = true }
.execute { // 1
userRepository.findById("...") // 2
}
val ride = rideRepository.findById("...")
// update something
rideRepository.save(ride) // 3
}
}
  1. @Transactional 어노테이션이 AOP를 통해 메서드 실행 전에 Transaction Context 를 새로 만듭니다.

    • JpaTransactionManager 에서 JdbcConnection 을 접근하면서 database 와 실제 database connection 을 맺는다.

      ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
      if (conHandle != null) {
      ConnectionHolder conHolder = new ConnectionHolder(conHandle);
      TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
      txObject.setConnectionHolder(conHolder);
      }
    • 근데 여기서 주의할 점은, 현재 타다 프로젝트에서는 LazyConnectionDataSourceProxy 를 사용하고 있습니다. LazyConnectionDataSourceProxy 는 Connection 객체를 만들때 실제로 database 에 연결하지 않고 proxy 객체만 만듭니다. 그리고 데이터에 접근할 때 connection 을 맺습니다.

      class PrimaryDatabaseConfiguration() {
      @Bean
      fun mainDataSource(): DataSource {
      return ReadReplicaAwareDataSourceFactory(dataSource = /* master db */, replicaDataSource = /* replica db */).createInstance()
      }
      }

      // LazyConnectionDataSourceProxy 을 사용
      class ReadReplicaAwareDataSourceFactory(private val dataSource: DataSource, private val replicaDataSource: DataSource,) : DataSourceFactory {
      override fun createInstance(): DataSource {
      val readOnlyRoutingDataSource = ReadOnlyRoutingDataSource()
      readOnlyRoutingDataSource.setTargetDataSources(replicaMap) // replicaMap is ["slave": replicaDataSource]
      readOnlyRoutingDataSource.setDefaultTargetDataSource(dataSource)
      return LazyConnectionDataSourceProxy(readOnlyRoutingDataSource)
      }
      }
    • LazyConnectionDataSourceProxy 는 connection 을 한번 만든 다음 캐싱해놓습니다.

      // LazyConnectionDataSourceProxy.java
      private Connection getTargetConnection(Method operation) throws SQLException {
      if (this.target == null) {
      // ...
      this.target = (this.username != null) ? obtainTargetDataSource().getConnection(this.username, this.password) : obtainTargetDataSource().getConnection();
      // ...
      }
      }
  2. 내부 transactionTemplate으로 새로운 트랜잭션을 시작하며 처음 데이터에 접근합니다. 이때 isReadReplicaEnabled 옵션이 true로 설정되어 있으니 determineCurrentLookupKey 함수를 통해 ReadReplica database 로 연결을 맺습니다.

    class ReadOnlyRoutingDataSource : AbstractRoutingDataSource() {
    // defaultTargetDataSource is "master" database.
    private var dataSourceKeys: List<Any>? = null // "slave"

    override fun determineCurrentLookupKey(): Any? {
    return if (isReadReplicaEnabled && dataSourceKeys!!.isNotEmpty()) {
    val randomDataSourceKey = dataSourceKeys!![getRandom(this.dataSourceKeys!!.size)]
    return randomDataSourceKey
    } else null
    }
    }
  3. 이후 ReadReplica 데이터베이스에 update 명령어를 요청하니, read only 옵션에서 수행할 수 없다고 에러가 발생합니다.


정리

이번 문제들을 통해 Spring의 Transaction 관리가 단순히 @Transactional 어노테이션이나 TransactionTemplate을 사용하는 것 이상의 복잡한 메커니즘으로 동작한다는 것을 깊이 이해하게 되었습니다. Transaction 컨텍스트가 ThreadLocal에 저장되고 TransactionSynchronizationManager를 통해 관리된다는 점, 그리고 여러 데이터베이스를 사용할 때는 각 EntityManagerFactory별로 독립적으로 관리된다는 것을 알게 되었습니다. 또한 LazyConnectionDataSourceProxy를 사용할 때는 Connection이 한 번 생성되면 캐싱되므로, Transaction 초기가 아닌 첫 데이터 접근 시점의 설정이 전체 Transaction에 영향을 미친다는 것도 알게 됐습니다. 여러 데이터베이스를 사용하거나 ReadReplica를 활용하는 복잡한 환경에서 관련된 이해가 많은 도움이 되었습니다.

Auto Lotto Bot

· 3 min read

회사를 다니기 막 시작할 때 지인들이 로또를 매주 구매하는게 낭비라고 생각했었다. 매우 낮은 확률에 일주일에 5천원씩, 한달에 약 3만원 정도를 소비하는게 꽤 큰 금액이라고 생각했다.

요즘 들어서 아무 일도 일어나지 않는 확률 0 보다는 가능성이라도 있는 0.0001% 확률이 낫다고 생각이 들었다. 일주일에 5천원이라 스타벅스 커피 한잔 더 마셨다 자기합리화를 하고 구매를 하기 시작했다.

meme

Synchronization Techniques in Game Development

· 14 min read

모빌리티 플랫폼 '타다' 개발에서 탑승 플로우(호출 - 호출 중 - 매칭 - 탑승 - 탑승 완료)를 앱에서 잘 보여주는 것이 매우 중요합니다. 호출을 하고 있다가 시간 만료로 취소가 되거나 매칭이 되었지만 드라이버가 사정이 생겨 취소하는 경우 등 탑승자의 아무런 입력 없이도 앱에서 탑승 상태를 잘 나타내야 합니다. 서버에 탑승 상태를 모바일 앱에서 잘 보여주기 위한 방법들을 고민하다가 멀티플레이어 게임에서의 어떻게 다른 플레이어들과 동시간대에 있는 것처럼 동기화가 잘되는지가 궁금해졌습니다. 멀티플레이어 게임을 하다보면 다양한 플레이어들의 상호작용이 있는데 이걸 어떻게 끊김 없이 보여주고 있는지 궁금해져서 관련된 기술들을 찾아서 정리해봤습니다.

UILabel Alignment

· 8 min read

디자이너 분들과 일할 때 AutoLayout Constraint 를 제대로 걸었음에도 "중앙정렬이 안맞는 것 같아요. 여기 텍스트 정렬이 어색해요" 와 같은 얘기를 들을 때가 종종 있었습니다. 실제로 UILabel 은 지정한 Font 크기보다 크게 잡히며 중앙정렬이 안되어 있는 것처럼 보였습니다. 이와 관련해서 왜 더 크게 잡히는지, 정렬이 어떤 식으로 동작하는지, Font 가 Label 에 어떤 영향을 미치는지 간단한 용어와 개념부터 소개하겠습니다.

How To Open An App By URL

· 7 min read

Question About Deeplink 최근에 서비스에서 중요한 기능을 개발하면서 해당 화면으로 사람들을 유도할 수 있는 링크에 대한 질문을 많이 받았습니다. 이와 관련된 답변을 준비하다 보니 url 을 통해 어떻게 설치되어 있으면 앱을 열고 설치되어 있지 않은 앱들은 앱스토어로 보내는 지에 대한 이해가 부족하다고 느꼈습니다. 관련된 3rd party 서비스 Branch 가 어떤 기능들을 제공해주는지에 대한 이해도 필요했습니다. 관련된 기능을 개발해본 적 없는 모바일 개발자 혹은 모바일 플랫폼에 대한 이해도가 부족한 서버 개발자분들의 이해를 돕고자 간단하게 정리했습니다. 설명은 iOS 중심으로 정리되어 있습니다.

Memory Management in Swift

· 12 min read

영어 읽는 습관을 기르고 기술적인 내용을 많이 접하기 위해 Medium 글들을 많이 읽으려고 노력합니다. 많은 글들을 접하던 중 Can You Answer This Simple Swift Question Correctly? 글을 읽었는데 Closure 안에서 변수 접근에 대한 질문에 많은 사람들이 제대로 답을 하지 못했다 라고 합니다. 평소에 중요하게 생각했던 부분이었고 면접 준비를 위해 정리하기로 했습니다.

SwiftUI - Data Flow Patterns

· 6 min read

SwiftUI 에서 View 는 SwiftUI 가 무엇을 어떻게 그릴 것인지 에 대해 명시한 가상의 객체입니다. 다양한 데이터를 보여주기 위해 SwiftUI 에서는 여러 Component 를 만들고 Component 들 간의 데이터 전달하는 과정이 필요합니다. 이번 글에서는 SwiftUI 에서 Parent 와 Child 간 데이터를 전달하는 방법들을 정리해봤습니다.

SwiftUI - Layout System

· 5 min read

최근에 SwiftUI 를 이용해 앱을 만들어 보면서 알게 된 사실들을 정리해보려 합니다. 이번 글은 View 사이즈를 계산하고 배치하는 방법과 과정에 대해 정리해봤습니다.

AlphaGo Paper Review (2)

· 5 min read

이세돌과의 경기로 AlphaGo Lee 에 대한 존재와 원리는 어느 정도 알고는 있었지만, 다음 버전 AlphaGo Zero 에 대해서는 상대적으로 관심이 적어 찾아볼 생각을 하지 않았었습니다. AlphaGo Lee 를 공부하게 되면서 AlphaGo Zero 에 대해서도 찾아보고 정리를 하게 되었습니다.